diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 63b65901a2..5f0d8b01d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,7 @@ updates: # and because it submits 3 different PRs - dependency-name: "Microsoft.EntityFrameworkCore.*" - dependency-name: "Microsoft.Extensions.*" - - dependency-name: "Npgsql" + - dependency-name: "HuaweiCloud.Driver.GaussDB" schedule: interval: daily diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd14700af5..c3d017cdfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,48 +60,70 @@ jobs: run: dotnet build --configuration Debug shell: bash - - name: Start PostgreSQL ${{ matrix.pg_major }} (Linux) + - name: Start GaussDB ${{ matrix.pg_major }} (Linux) if: startsWith(matrix.os, 'ubuntu') run: | - # First uninstall any PostgreSQL installed on the image - dpkg-query -W --showformat='${Package}\n' 'postgresql-*' | xargs sudo dpkg -P postgresql + # First uninstall any GaussDB installed on the image + dpkg-query -W --showformat='${Package}\n' 'opengauss-*' | xargs sudo dpkg -P opengauss # Automated repository configuration - sudo apt install -y postgresql-common - sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v ${{ matrix.pg_major }} -y + sudo apt install -y opengauss-common + sudo /usr/share/opengauss-common/pgdg/apt.opengauss.org.sh -v ${{ matrix.pg_major }} -y sudo apt-get update -qq - sudo apt-get install -qq postgresql-${{ matrix.pg_major }} + sudo apt-get install -qq opengauss-${{ matrix.pg_major }} # To disable PostGIS for prereleases (because it usually isn't available until late), surround with the following: #if [ -z "${{ matrix.pg_prerelease }}" ]; then - sudo apt-get install -qq postgresql-${{ matrix.pg_major }}-postgis-${{ env.postgis_version }} + sudo apt-get install -qq opengauss-${{ matrix.pg_major }}-postgis-${{ env.postgis_version }} #fi - sudo sed -i 's/max_connections = 100/max_connections = 200/g' /etc/postgresql/${{ matrix.pg_major }}/main/postgresql.conf - sudo systemctl restart postgresql + sudo sed -i 's/max_connections = 100/max_connections = 200/g' /etc/opengauss/${{ matrix.pg_major }}/main/opengauss.conf + sudo systemctl restart opengauss sudo -u postgres psql -c "CREATE USER npgsql_tests SUPERUSER PASSWORD 'npgsql_tests'" - - name: Start PostgreSQL ${{ matrix.pg_major }} (Windows) + - name: Start GaussDB ${{ matrix.pg_major }} (Windows) if: startsWith(matrix.os, 'windows') run: | # Find EnterpriseDB version number EDB_VERSION=$(pwsh -c " \$global:progressPreference='silentlyContinue'; - Invoke-WebRequest -URI https://www.postgresql.org/applications-v2.xml | + Invoke-WebRequest -URI https://www.opengauss.org/applications-v2.xml | Select-Object -ExpandProperty Content | - Select-Xml -XPath '/applications/application[id=\"postgresql_${{ matrix.pg_major }}\" and platform=\"windows-x64\"]/version/text()' | + Select-Xml -XPath '/applications/application[id=\"opengauss_${{ matrix.pg_major }}\" and platform=\"windows-x64\"]/version/text()' | Select-Object -First 1 -ExpandProperty Node | Select-Object -ExpandProperty Value") - # Install PostgreSQL - echo "Installing PostgreSQL (version: ${EDB_VERSION})" - curl -o pgsql.zip -L https://get.enterprisedb.com/postgresql/postgresql-${EDB_VERSION}-windows-x64-binaries.zip + # Install GaussDB + echo "Installing GaussDB (version: ${EDB_VERSION})" + curl -o pgsql.zip -L https://get.enterprisedb.com/opengauss/opengauss-${EDB_VERSION}-windows-x64-binaries.zip unzip pgsql.zip -x 'pgsql/include/**' 'pgsql/doc/**' 'pgsql/pgAdmin 4/**' 'pgsql/StackBuilder/**' - # Match Npgsql CI Docker image and stash one level up + # Match GaussDB CI Docker image and stash one level up cp $GITHUB_WORKSPACE/.build/{server.crt,server.key} pgsql - # Start PostgreSQL + # Find OSGEO version number + OSGEO_VERSION=$(\ + curl -Ls https://download.osgeo.org/postgis/windows/pg${{ matrix.pg_major }} | + sed -n 's/.*>postgis-bundle-pg${{ matrix.pg_major }}-\(${{ env.postgis_version }}.[0-9]*.[0-9]*\)x64.zip<.*/\1/p' | + tail -n 1) + if [ -z "$OSGEO_VERSION" ]; then + OSGEO_VERSION=$(\ + curl -Ls https://download.osgeo.org/postgis/windows/pg${{ matrix.pg_major }}/archive | + sed -n 's/.*>postgis-bundle-pg${{ matrix.pg_major }}-\(${{ env.postgis_version }}.[0-9]*.[0-9]*\)x64.zip<.*/\1/p' | + tail -n 1) + POSTGIS_PATH="archive/" + else + POSTGIS_PATH="" + fi + + # Install PostGIS + echo "Installing PostGIS (version: ${OSGEO_VERSION})" + POSTGIS_FILE="postgis-bundle-pg${{ matrix.pg_major }}-${OSGEO_VERSION}x64" + curl -o postgis.zip -L https://download.osgeo.org/postgis/windows/pg${{ matrix.pg_major }}/${POSTGIS_FILE}.zip + unzip postgis.zip -d postgis + cp -a postgis/$POSTGIS_FILE/. pgsql/ + + # Start GaussDB pgsql/bin/initdb -D pgsql/PGDATA -E UTF8 -U postgres pgsql/bin/pg_ctl -D pgsql/PGDATA -l logfile -o '-c max_connections=200 -c max_prepared_transactions=10 -c ssl=true -c ssl_cert_file=../server.crt -c ssl_key_file=../server.key' start @@ -146,7 +168,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v6 with: - name: EFCore.PG.CI + name: EFCore.GaussDB.CI path: nupkgs - name: Publish packages to MyGet (vnext) @@ -178,7 +200,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v6 with: - name: EFCore.PG.Release + name: EFCore.GaussDB.Release path: nupkgs # TODO: Create a release diff --git a/.gitignore b/.gitignore index dbb844096e..ec5e244bf1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ artifacts/ TestResult.xml .dotnet .vscode/ +example/GetStarted/GetStarted/.env diff --git a/Directory.Build.props b/Directory.Build.props index 1643ba4198..e8127ca1b2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,28 +1,29 @@ - 11.0.0-preview.1 - net10.0 + 0.0.1 + net10.0 latest enable true latest + $(NoWarn);NU5105 true - $(MSBuildThisFileDirectory)Npgsql.snk + $(MSBuildThisFileDirectory)GaussDB.snk true true true true - Copyright 2025 © The Npgsql Development Team - Npgsql + Copyright 2026 © The GaussDB Development Team + GaussDB true PostgreSQL - https://github.com/npgsql/efcore.pg - postgresql.png + https://github.com/HuaweiCloudDeveloper/gaussdb-efcore + gaussdb.png - + @@ -51,8 +52,8 @@ - - + + diff --git a/Directory.Packages.props b/Directory.Packages.props index d5d1266684..c09d390ae2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,30 +1,24 @@ - + 10.0.0 10.0.0 - 10.0.0 + 0.1.0 - - - - - - - - + + + + + + + - - - - - - + + + + @@ -33,4 +27,4 @@ - + \ No newline at end of file diff --git a/EFCore.GaussDB.slnx b/EFCore.GaussDB.slnx new file mode 100644 index 0000000000..530bea5566 --- /dev/null +++ b/EFCore.GaussDB.slnx @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EFCore.PG.slnx.DotSettings b/EFCore.GaussDB.slnx.DotSettings similarity index 100% rename from EFCore.PG.slnx.DotSettings rename to EFCore.GaussDB.slnx.DotSettings diff --git a/EFCore.PG.slnx b/EFCore.PG.slnx deleted file mode 100644 index 02e7db8e67..0000000000 --- a/EFCore.PG.slnx +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Npgsql.snk b/GaussDB.snk similarity index 100% rename from Npgsql.snk rename to GaussDB.snk diff --git a/NuGet.config b/NuGet.config index f95d7a9e97..5d39099bd6 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,22 +1,22 @@ - + - - - - + + + + - - + + - - + + diff --git a/QueryBaseline.cs b/QueryBaseline.cs index 15a46b8add..ddec87b640 100644 --- a/QueryBaseline.cs +++ b/QueryBaseline.cs @@ -72,7 +72,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -120,7 +120,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -366,7 +366,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -414,7 +414,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -660,7 +660,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -708,7 +708,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -962,7 +962,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -1010,7 +1010,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -1288,7 +1288,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -1534,7 +1534,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -1582,7 +1582,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -1828,7 +1828,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -1876,7 +1876,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -2122,7 +2122,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsSingleline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 238 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"" @@ -2170,7 +2170,7 @@ -Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.PG.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 +Microsoft.EntityFrameworkCore.Query.SimpleQueryNpgsqlTest.Regex_IsMatchOptionsMultiline() : line() na R:\RafaelNuvem\ProjetosGit\EFCorePostgreSQL\test\EFCore.GaussDB.FunctionalTests\Query\SimpleQueryNpgsqlTest.Functions.cs:linha 229 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"" @@ -3958,7 +3958,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryAnd() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryAnd() : AssertSql( @"SELECT (to_tsquery('a & b') && to_tsquery('c & d')) FROM ""Customers"" AS c @@ -3966,7 +3966,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryOr() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryOr() : AssertSql( @"SELECT (to_tsquery('a & b') || to_tsquery('c & d')) FROM ""Customers"" AS c @@ -3974,7 +3974,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.Matches_With_String() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.Matches_With_String() : AssertSql( @"SELECT (to_tsvector('a') @@ 'b') FROM ""Customers"" AS c @@ -3982,7 +3982,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.Matches_With_Tsquery() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.Matches_With_Tsquery() : AssertSql( @"SELECT (to_tsvector('a') @@ to_tsquery('b')) FROM ""Customers"" AS c @@ -3990,7 +3990,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryIsContainedIn() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryIsContainedIn() : AssertSql( @"SELECT (to_tsquery('b') <@ to_tsquery('a & b')) FROM ""Customers"" AS c @@ -3998,7 +3998,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryContains() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.TsQueryContains() : AssertSql( @"SELECT (to_tsquery('a & b') @> to_tsquery('b')) FROM ""Customers"" AS c @@ -4006,7 +4006,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.TsVectorConcat() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.TsVectorConcat() : AssertSql( @"SELECT (to_tsvector('b') || to_tsvector('c')) FROM ""Customers"" AS c @@ -4014,7 +4014,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.SpatialQueryNpgsqlGeographyTest.d__9.MoveNext() : line 84 +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.SpatialQueryNpgsqlGeographyTest.d__9.MoveNext() : line 84 AssertSql( @"@__point_0='POINT (0 1)' (DbType = Object) @@ -4023,7 +4023,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.SpatialQueryNpgsqlGeographyTest.d__9.MoveNext() : line 84 +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.SpatialQueryNpgsqlGeographyTest.d__9.MoveNext() : line 84 AssertSql( @"@__point_0='POINT (0 1)' (DbType = Object) @@ -4048,7 +4048,7 @@ -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.WebSearchToTsQuery_With_Config_From_Variable() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.WebSearchToTsQuery_With_Config_From_Variable() : AssertSql( @"@__config_1='english' @@ -4058,7 +4058,7 @@ SELECT websearch_to_tsquery(CAST(@__config_1 AS regconfig), 'a OR b') -Npgsql.EntityFrameworkCore.PostgreSQL.Query.FullTextSearchDbFunctionsNpgsqlTest.WebSearchToTsQuery_With_Config() : +HuaweiCloud.EntityFrameworkCore.GaussDB.Query.FullTextSearchDbFunctionsNpgsqlTest.WebSearchToTsQuery_With_Config() : AssertSql( @"SELECT websearch_to_tsquery(CAST('english' AS regconfig), 'a OR b') FROM ""Customers"" AS c diff --git a/README.md b/README.md index a3edf26ef7..f60e2eb4cf 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ -# Npgsql Entity Framework Core provider for PostgreSQL +# GaussDB Entity Framework Core provider for GaussDB -[![stable](https://img.shields.io/nuget/v/Npgsql.EntityFrameworkCore.PostgreSQL.svg?label=stable)](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL/) -[![next patch](https://img.shields.io/myget/npgsql/v/Npgsql.EntityFrameworkCore.PostgreSQL.svg?label=next%20patch)](https://www.myget.org/feed/npgsql/package/nuget/Npgsql.EntityFrameworkCore.PostgreSQL) -[![daily builds (vnext)](https://img.shields.io/myget/npgsql-vnext/v/Npgsql.EntityFrameworkCore.PostgreSQL.svg?label=vNext)](https://www.myget.org/feed/npgsql-vnext/package/nuget/Npgsql.EntityFrameworkCore.PostgreSQL) -[![build](https://github.com/npgsql/efcore.pg/actions/workflows/build.yml/badge.svg)](https://github.com/npgsql/efcore.pg/actions/workflows/build.yml) -[![gitter](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg)](https://gitter.im/npgsql/npgsql) +[![stable](https://img.shields.io/nuget/v/HuaweiCloud.EntityFrameworkCore.GaussDB.svg?label=stable)](https://www.nuget.org/packages/HuaweiCloud.EntityFrameworkCore.GaussDB/) +[![next patch](https://img.shields.io/myget/npgsql/v/HuaweiCloud.EntityFrameworkCore.GaussDB.svg?label=next%20patch)](https://www.myget.org/feed/npgsql/package/nuget/HuaweiCloud.EntityFrameworkCore.GaussDB) +[![daily builds (vnext)](https://img.shields.io/myget/npgsql-vnext/v/HuaweiCloud.EntityFrameworkCore.GaussDB.svg?label=vNext)](https://www.myget.org/feed/npgsql-vnext/package/nuget/HuaweiCloud.EntityFrameworkCore.GaussDB) +[![build](https://github.com/HuaweiCloudDeveloper/gaussdb-efcore/actions/workflows/build.yml/badge.svg)](https://github.com/HuaweiCloudDeveloper/gaussdb-efcore/actions/workflows/build.yml) -Npgsql.EntityFrameworkCore.PostgreSQL is the open source EF Core provider for PostgreSQL. It allows you to interact with PostgreSQL via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. It's built on top of [Npgsql](https://github.com/npgsql/npgsql). +HuaweiCloud.EntityFrameworkCore.GaussDB is the open source EF Core provider for GaussDB. It allows you to interact with GaussDB via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. It's built on top of [GaussDB](https://github.com/HuaweiCloudDeveloper/gaussdb-dotnet). The provider looks and feels just like any other Entity Framework Core provider. Here's a quick sample to get you started: @@ -27,7 +26,7 @@ public class BlogContext : DbContext public DbSet Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(@"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase"); + => optionsBuilder.UseGaussDB(@"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase"); } public class Blog @@ -37,10 +36,10 @@ public class Blog } ``` -Aside from providing general EF Core support for PostgreSQL, the provider also exposes some PostgreSQL-specific capabilities, allowing you to query JSON, array or range columns, as well as many other advanced features. For more information, see the [the Npgsql site](http://www.npgsql.org/efcore/index.html). For information about EF Core in general, see the [EF Core website](https://docs.microsoft.com/ef/core/). +Aside from providing general EF Core support for GaussDB, the provider also exposes some GaussDB-specific capabilities, allowing you to query JSON, array or range columns, as well as many other advanced features. For more information, see the [the GaussDB site](https://doc.hcs.huawei.com/db/zh-cn/index.html). For information about EF Core in general, see the [EF Core website](https://docs.microsoft.com/ef/core/). ## Related packages -* Spatial plugin to work with PostgreSQL PostGIS: [Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite) -* NodaTime plugin to use better date/time types with PostgreSQL: [Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime) -* The underlying Npgsql ADO.NET provider is [Npgsql](https://www.nuget.org/packages/Npgsql). +* Spatial plugin to work with GaussDB PostGIS: [HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite](https://www.nuget.org/packages/HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite) +* NodaTime plugin to use better date/time types with GaussDB: [HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime](https://www.nuget.org/packages/HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime) +* The underlying GaussDB ADO.NET provider is [GaussDB](https://www.nuget.org/packages/HuaweiCloud.Driver.GaussDB/). diff --git a/example/GetStarted/GetStarted/.env-example b/example/GetStarted/GetStarted/.env-example new file mode 100644 index 0000000000..ae3289e61e --- /dev/null +++ b/example/GetStarted/GetStarted/.env-example @@ -0,0 +1 @@ +GaussDBConnString= \ No newline at end of file diff --git a/example/GetStarted/GetStarted/Employee.cs b/example/GetStarted/GetStarted/Employee.cs new file mode 100644 index 0000000000..5398694d9f --- /dev/null +++ b/example/GetStarted/GetStarted/Employee.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace GetStarted +{ + /// + /// 定义员工实体类,映射到 GaussDB 数据库中的 "employees" 表 + /// + public class Employee + { + /// + /// 主键 + /// + [Column("id")] + public int Id { get; set; } + + /// + /// 员工姓名,最长 128 字符 + /// + [Column("name")] + public string Name { get; set; } = default!; + + /// + /// 员工年龄 + /// + [Column("age")] + public int Age { get; set; } + } + +} diff --git a/example/GetStarted/GetStarted/GaussDBDbContext.cs b/example/GetStarted/GetStarted/GaussDBDbContext.cs new file mode 100644 index 0000000000..244937371e --- /dev/null +++ b/example/GetStarted/GetStarted/GaussDBDbContext.cs @@ -0,0 +1,47 @@ +using System; +using dotenv.net; + +namespace GetStarted +{ + /// + /// EF Core 上下文类,用于配置和操作 GaussDB 数据库 + /// + public class GaussDBDbContext(DbContextOptions options) : DbContext(options) + { + + /// + /// 员工表,EF Core 会自动根据模型类进行映射 + /// + public DbSet Employees { get; set; } = null!; + + /// + /// 连接字符串配置 + /// + /// + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + + DotEnv.Load(); // 加载 .env 文件中的环境变量 + + // 从环境变量中读取连接字符串(需通过 dotenv 或系统环境变量设置) + var connString = Environment.GetEnvironmentVariable("GaussDBConnString") + ?? throw new InvalidOperationException("未找到 GaussDBConnString 环境变量"); + + // 使用 GaussDB 的 EF Core provider(确保你已安装 UseGaussDB 的 NuGet 扩展) + optionsBuilder.UseGaussDB(connString); + } + + /// + /// 定义实体与表的映射关系 + /// + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("employees"); // 映射表名 + modelBuilder.Entity().HasKey(e => e.Id); // 设置主键 + modelBuilder.Entity().Property(e => e.Name).HasMaxLength(128); // 设置字段长度 + } + } + +} diff --git a/example/GetStarted/GetStarted/GetStarted.csproj b/example/GetStarted/GetStarted/GetStarted.csproj new file mode 100644 index 0000000000..9ea932d17a --- /dev/null +++ b/example/GetStarted/GetStarted/GetStarted.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + enable + disable + $(NoWarn);CS8002 + + + + + + + + + + + + + Always + + + + diff --git a/example/GetStarted/GetStarted/Program.cs b/example/GetStarted/GetStarted/Program.cs new file mode 100644 index 0000000000..68196f293d --- /dev/null +++ b/example/GetStarted/GetStarted/Program.cs @@ -0,0 +1,125 @@ +using System.Data; +using dotenv.net; +using GetStarted; + +DotEnv.Load(); // 加载 .env 文件中的环境变量 + +var connString = Environment.GetEnvironmentVariable("GaussDBConnString"); +ArgumentNullException.ThrowIfNull(connString); +// ReSharper disable AccessToDisposedClosure +// Dispose will be handled +await using var conn = new GaussDBConnection(connString); +if (conn.State is ConnectionState.Closed) +{ + await conn.OpenAsync(); +} + +Console.WriteLine($@"Connection state: {conn.State}"); + + +// 配置DbContext +var optionsBuilder = new DbContextOptionsBuilder(); +optionsBuilder.UseGaussDB(connString) + .LogTo(Console.WriteLine) // 开启日志,便于排查详细错误 + .EnableSensitiveDataLogging(); // 开发环境临时开启,生产环境关闭 + +await using var ctx = new GaussDBDbContext(optionsBuilder.Options); + +// ⚠️开发时建议使用:清空旧表并重新建表 +try +{ + if (await ctx.Database.CanConnectAsync()) + { + Console.WriteLine("📦 检测到数据库存在,准备删除..."); + await ctx.Database.EnsureDeletedAsync(); // 删除数据库 + Console.WriteLine("✅ 数据库已成功删除"); + } + else + { + Console.WriteLine("ℹ️ 数据库不存在,无需删除"); + } +} +catch (Exception e) +{ + Console.WriteLine(e.Message + e.StackTrace); +} + +// 然后创建数据库结构 +var CreateSuccess = await ctx.Database.EnsureCreatedAsync(); // 创建数据库结构 +Console.WriteLine("✅ 数据库结构已创建" + CreateSuccess); + +if (!CreateSuccess) +{ + // ✅ 手动创建表 + await CreateTable(ctx); +} + +// 插入初始数据 +ctx.Employees?.AddRange( + new Employee { Name = "John", Age = 30 }, + new Employee { Name = "Alice", Age = 16 }, + new Employee { Name = "Mike", Age = 24 } +); +await ctx.SaveChangesAsync(); // 提交更改 +Console.WriteLine("✅ 初始数据已插入"); + +// 查询所有员工 +await QueryTest(ctx); +// 更新 Alice 的年龄为 18 +var alice = await ctx.Employees.FirstOrDefaultAsync(e => e.Name == "Alice"); +if (alice != null) +{ + alice.Age = 18; + await ctx.SaveChangesAsync(); + Console.WriteLine("✅ 已更新 Alice 的年龄为 18"); +} + +// 查询年龄大于等于 18 的员工 +await QueryTest(ctx, e => e.Age >= 18); + +// 删除年龄大于 10 的员工 +var toDelete = await ctx.Employees.Where(e => e.Age > 10).ToListAsync(); +ctx.Employees.RemoveRange(toDelete); +await ctx.SaveChangesAsync(); +Console.WriteLine("✅ 删除了年龄 > 10 的员工"); + +// 查询剩余人数 +var count = await ctx.Employees.CountAsync(); +Console.WriteLine($"📊 当前员工总数: {count}"); + +Console.WriteLine("🎉 所有操作已完成!"); + + +// 封装查询方法,可传入条件表达式 +static async Task QueryTest(GaussDBDbContext ctx, Expression> predicate = null) +{ + var query = ctx.Employees?.AsQueryable(); + + // 如果有条件,则筛选 + if (predicate != null) + query = query?.Where(predicate); + + if (query == null) + return; + + var results = await query.OrderBy(s => s.Age).ToListAsync(); + foreach (var e in results) + { + Console.WriteLine($"👤 ID: {e.Id}, Name: {e.Name}, Age: {e.Age}"); + } +} + +async Task CreateTable(GaussDBDbContext ctx) +{ + const string createSql = """ + CREATE TABLE IF NOT EXISTS employees ( + id INT PRIMARY KEY, + name VARCHAR(128), + age INT + ); + """; + + await ctx.Database.ExecuteSqlRawAsync(createSql); + Console.WriteLine("✅ 创建表 employees 完成"); +} + diff --git a/gaussdb.png b/gaussdb.png new file mode 100644 index 0000000000..efbd2cd9c4 Binary files /dev/null and b/gaussdb.png differ diff --git a/global.json b/global.json index 6a6aeda3e2..b48eef46e3 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "10.0.100", - "rollForward": "latestMinor", - "allowPrerelease": false + "rollForward": "latestMajor", + "allowPrerelease": true } } diff --git a/postgresql.png b/postgresql.png deleted file mode 100644 index 3a21b19f7a..0000000000 Binary files a/postgresql.png and /dev/null differ diff --git a/src/EFCore.GaussDB.NTS/Design/Internal/GaussDBNetTopologySuiteDesignTimeServices.cs b/src/EFCore.GaussDB.NTS/Design/Internal/GaussDBNetTopologySuiteDesignTimeServices.cs new file mode 100644 index 0000000000..9a7472cd39 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Design/Internal/GaussDBNetTopologySuiteDesignTimeServices.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Design.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 GaussDBNetTopologySuiteDesignTimeServices : IDesignTimeServices +{ + /// + /// 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 void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + => serviceCollection + .AddSingleton() + .AddSingleton() + .TryAddSingleton(); +} diff --git a/src/EFCore.GaussDB.NTS/EFCore.GaussDB.NTS.csproj b/src/EFCore.GaussDB.NTS/EFCore.GaussDB.NTS.csproj new file mode 100644 index 0000000000..1078c971a0 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/EFCore.GaussDB.NTS.csproj @@ -0,0 +1,34 @@ + + + HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite + HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite + + Shay Rojansky + NetTopologySuite PostGIS spatial support plugin for GaussDB Entity Framework Core provider. + npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;spatial;postgis;nts + README.md + + + + + + + + + + + + + + + + + + + + True + build + + + + diff --git a/src/EFCore.GaussDB.NTS/EFCore.GaussDB.NTS.csproj.Backup.tmp b/src/EFCore.GaussDB.NTS/EFCore.GaussDB.NTS.csproj.Backup.tmp new file mode 100644 index 0000000000..04175bb3be --- /dev/null +++ b/src/EFCore.GaussDB.NTS/EFCore.GaussDB.NTS.csproj.Backup.tmp @@ -0,0 +1,34 @@ + + + HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite + HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite + + Shay Rojansky + NetTopologySuite PostGIS spatial support plugin for GaussDB Entity Framework Core provider. + npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;spatial;postgis;nts + README.md + + + + + + + + + + + + + + + + + + + + True + build + + + + diff --git a/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions.cs b/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000000..db7d77c550 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,54 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// NetTopologySuite specific extension methods for . +/// +public static class GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions +{ + /// + /// Use NetTopologySuite to access SQL Server spatial data. + /// + /// + /// The options builder so that further configuration can be chained. + /// + public static GaussDBDbContextOptionsBuilder UseNetTopologySuite( + this GaussDBDbContextOptionsBuilder optionsBuilder, + CoordinateSequenceFactory? coordinateSequenceFactory = null, + PrecisionModel? precisionModel = null, + Ordinates handleOrdinates = Ordinates.None, + bool geographyAsDefault = false) + { + var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; + + var extension = coreOptionsBuilder.Options.FindExtension() + ?? new GaussDBNetTopologySuiteOptionsExtension(); + + 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(); + } + + ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } +} diff --git a/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteDbFunctionsExtensions.cs b/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteDbFunctionsExtensions.cs new file mode 100644 index 0000000000..85e2e4b54b --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteDbFunctionsExtensions.cs @@ -0,0 +1,76 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides GaussDB-specific spatial extension methods on . +/// +public static class GaussDBNetTopologySuiteDbFunctionsExtensions +{ + /// + /// Returns a new geometry with its coordinates transformed to a different spatial reference system. + /// Translates to ST_Transform(geometry, srid). + /// + /// + /// See https://postgis.net/docs/ST_Transform.html. + /// + public static TGeometry Transform(this DbFunctions _, TGeometry geometry, int srid) + where TGeometry : Geometry + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Transform))); + + /// + /// Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. + /// Translates to ST_Force2D(geometry) + /// + /// + /// See https://postgis.net/docs/ST_Force2D.html. + /// + public static TGeometry Force2D(this DbFunctions _, TGeometry geometry) + where TGeometry : Geometry + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Force2D))); + + /// + /// Tests whether the distance from the origin geometry to another is less than or equal to a specified value. + /// Translates to ST_DWithin. + /// + /// + /// See https://postgis.net/docs/ST_DWithin.html. + /// + /// The instance. + /// The origin geometry. + /// The geometry to check the distance to. + /// The distance value to compare. + /// Whether to use sphere or spheroid distance measurement. + /// if the geometries are less than distance apart. + public static bool IsWithinDistance(this DbFunctions _, Geometry geometry, Geometry anotherGeometry, double distance, bool useSpheroid) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsWithinDistance))); + + /// + /// Returns the minimum distance between the origin geometry and another geometry g. + /// Translates to ST_Distance. + /// + /// + /// See https://postgis.net/docs/ST_Distance.html. + /// + /// The instance. + /// The origin geometry. + /// The geometry from which to compute the distance. + /// Whether to use sphere or spheroid distance measurement. + /// The distance between the geometries. + public static double Distance(this DbFunctions _, Geometry geometry, Geometry anotherGeometry, bool useSpheroid) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Returns the 2D distance between two geometries. Used in the "ORDER BY" clause, provides index-assisted nearest-neighbor result + /// sets. Translates to <->. + /// + /// + /// See https://postgis.net/docs/ST_Distance.html. + /// + /// The instance. + /// The origin geometry. + /// The geometry from which to compute the distance. + /// The 2D distance between the geometries. + public static double DistanceKnn(this DbFunctions _, Geometry geometry, Geometry anotherGeometry) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceKnn))); +} diff --git a/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteServiceCollectionExtensions.cs new file mode 100644 index 0000000000..ad29b3d5ba --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Extensions/GaussDBNetTopologySuiteServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite extension methods for . +/// +public static class GaussDBNetTopologySuiteServiceCollectionExtensions +{ + /// + /// Adds the services required for NetTopologySuite support in the GaussDB provider for Entity Framework. + /// + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddEntityFrameworkGaussDBNetTopologySuite( + this IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + new EntityFrameworkGaussDBServicesBuilder(serviceCollection) + .TryAdd() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAddProviderSpecificServices( + x => x.TryAddSingleton()); + + return serviceCollection; + } +} diff --git a/src/EFCore.GaussDB.NTS/Infrastructure/Internal/GaussDBNetTopologySuiteOptionsExtension.cs b/src/EFCore.GaussDB.NTS/Infrastructure/Internal/GaussDBNetTopologySuiteOptionsExtension.cs new file mode 100644 index 0000000000..4f371e34b7 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Infrastructure/Internal/GaussDBNetTopologySuiteOptionsExtension.cs @@ -0,0 +1,232 @@ +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBNetTopologySuiteOptionsExtension : 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 + /// 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 bool IsGeographyDefault { 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 GaussDBNetTopologySuiteOptionsExtension() { } + + /// + /// 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 GaussDBNetTopologySuiteOptionsExtension(GaussDBNetTopologySuiteOptionsExtension copyFrom) + { + IsGeographyDefault = copyFrom.IsGeographyDefault; + } + + /// + /// 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 GaussDBNetTopologySuiteOptionsExtension Clone() + => new(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 void ApplyServices(IServiceCollection services) + => services.AddEntityFrameworkGaussDBNetTopologySuite(); + + /// + /// 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 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 GaussDBNetTopologySuiteOptionsExtension 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 GaussDBNetTopologySuiteOptionsExtension 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 GaussDBNetTopologySuiteOptionsExtension 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 + /// 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 GaussDBNetTopologySuiteOptionsExtension WithGeographyDefault(bool isGeographyDefault = true) + { + var clone = Clone(); + + clone.IsGeographyDefault = isGeographyDefault; + + 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 void Validate(IDbContextOptions options) + { + Check.NotNull(options, nameof(options)); + + var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; + if (internalServiceProvider is not null) + { + using (var scope = internalServiceProvider.CreateScope()) + { + if (scope.ServiceProvider.GetService>() + ?.Any(s => s is GaussDBNetTopologySuiteTypeMappingSourcePlugin) + != true) + { + throw new InvalidOperationException( + $"{nameof(GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite)} requires {nameof(GaussDBNetTopologySuiteServiceCollectionExtensions.AddEntityFrameworkGaussDBNetTopologySuite)} to be called on the internal service provider used."); + } + } + } + } + + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) + { + private string? _logFragment; + + private new GaussDBNetTopologySuiteOptionsExtension Extension + => (GaussDBNetTopologySuiteOptionsExtension)base.Extension; + + public override bool IsDatabaseProvider + => false; + + public override int GetServiceProviderHashCode() + => Extension.IsGeographyDefault.GetHashCode(); + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => 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) + { + Check.NotNull(debugInfo, nameof(debugInfo)); + + var prefix = "GaussDB:" + nameof(GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite); + debugInfo[prefix] = "1"; + debugInfo[$"{prefix}:{nameof(IsGeographyDefault)}"] = Extension.IsGeographyDefault.ToString(); + } + + public override string LogFragment + { + get + { + if (_logFragment is null) + { + var builder = new StringBuilder("using NetTopologySuite"); + if (Extension.IsGeographyDefault) + { + builder.Append(" (geography by default)"); + } + + builder.Append(' '); + + _logFragment = builder.ToString(); + } + + return _logFragment; + } + } + } +} diff --git a/src/EFCore.GaussDB.NTS/Infrastructure/Internal/IGaussDBNetTopologySuiteSingletonOptions.cs b/src/EFCore.GaussDB.NTS/Infrastructure/Internal/IGaussDBNetTopologySuiteSingletonOptions.cs new file mode 100644 index 0000000000..412a687795 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Infrastructure/Internal/IGaussDBNetTopologySuiteSingletonOptions.cs @@ -0,0 +1,41 @@ +// ReSharper disable once CheckNamespace + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +/// +/// Represents options for GaussDB NetTopologySuite that can only be set at the singleton level. +/// +public interface IGaussDBNetTopologySuiteSingletonOptions : 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.GaussDB.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs similarity index 79% rename from src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs rename to src/EFCore.GaussDB.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs index e8b06a8353..d8a5187cf3 100644 --- a/src/EFCore.PG.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs +++ b/src/EFCore.GaussDB.NTS/Infrastructure/Internal/NetTopologySuiteDataSourceConfigurationPlugin.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -6,8 +6,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.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 NetTopologySuiteDataSourceConfigurationPlugin(INpgsqlNetTopologySuiteSingletonOptions options) - : INpgsqlDataSourceConfigurationPlugin +public class NetTopologySuiteDataSourceConfigurationPlugin(IGaussDBNetTopologySuiteSingletonOptions options) + : IGaussDBDataSourceConfigurationPlugin { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -15,7 +15,7 @@ public class NetTopologySuiteDataSourceConfigurationPlugin(INpgsqlNetTopologySui /// 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) + public void Configure(GaussDBDataSourceBuilder npgsqlDataSourceBuilder) => npgsqlDataSourceBuilder.UseNetTopologySuite( options.CoordinateSequenceFactory, options.PrecisionModel, diff --git a/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteConventionSetPlugin.cs b/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteConventionSetPlugin.cs new file mode 100644 index 0000000000..4ee9657882 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteConventionSetPlugin.cs @@ -0,0 +1,25 @@ +// ReSharper disable once CheckNamespace + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBNetTopologySuiteConventionSetPlugin : IConventionSetPlugin +{ + /// + /// 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 ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.ModelFinalizingConventions.Add(new GaussDBNetTopologySuiteExtensionAddingConvention()); + + return conventionSet; + } +} diff --git a/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteExtensionAddingConvention.cs b/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteExtensionAddingConvention.cs new file mode 100644 index 0000000000..1a12069ff2 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteExtensionAddingConvention.cs @@ -0,0 +1,21 @@ +// ReSharper disable once CheckNamespace + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBNetTopologySuiteExtensionAddingConvention : IModelFinalizingConvention +{ + /// + /// 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 void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + => modelBuilder.HasPostgresExtension("postgis"); +} diff --git a/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteSingletonOptions.cs b/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteSingletonOptions.cs new file mode 100644 index 0000000000..5f0215a6e3 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Internal/GaussDBNetTopologySuiteSingletonOptions.cs @@ -0,0 +1,35 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; + +/// +public class GaussDBNetTopologySuiteSingletonOptions : IGaussDBNetTopologySuiteSingletonOptions +{ + /// + public virtual CoordinateSequenceFactory? CoordinateSequenceFactory { get; set; } + + /// + public virtual PrecisionModel? PrecisionModel { get; set; } + + /// + public virtual Ordinates HandleOrdinates { get; set; } + + /// + public virtual bool IsGeographyDefault { get; set; } + + /// + public virtual void Initialize(IDbContextOptions options) + { + var npgsqlNtsOptions = options.FindExtension() + ?? new GaussDBNetTopologySuiteOptionsExtension(); + + CoordinateSequenceFactory = npgsqlNtsOptions.CoordinateSequenceFactory; + PrecisionModel = npgsqlNtsOptions.PrecisionModel; + HandleOrdinates = npgsqlNtsOptions.HandleOrdinates; + IsGeographyDefault = npgsqlNtsOptions.IsGeographyDefault; + } + + /// + public virtual void Validate(IDbContextOptions options) { } +} diff --git a/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs new file mode 100644 index 0000000000..5ecf7343b3 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs @@ -0,0 +1,155 @@ +using NetTopologySuite.Algorithm; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Union; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin +{ + /// + /// 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 GaussDBNetTopologySuiteAggregateMethodCallTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + { + if (sqlExpressionFactory is not GaussDBExpressionFactory npgsqlSqlExpressionFactory) + { + throw new ArgumentException($"Must be an {nameof(GaussDBExpressionFactory)}", nameof(sqlExpressionFactory)); + } + + Translators = + [ + new GaussDBNetTopologySuiteAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) + ]; + } + + /// + /// 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 IEnumerable Translators { 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 class GaussDBNetTopologySuiteAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private static readonly MethodInfo GeometryCombineMethod + = typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), [typeof(IEnumerable)])!; + + private static readonly MethodInfo ConvexHullMethod + = typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), [typeof(IEnumerable)])!; + + private static readonly MethodInfo UnionMethod + = typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), [typeof(IEnumerable)])!; + + private static readonly MethodInfo EnvelopeCombineMethod + = typeof(EnvelopeCombiner).GetRuntimeMethod(nameof(EnvelopeCombiner.CombineAsGeometry), [typeof(IEnumerable)])!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + + /// + /// 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 GaussDBNetTopologySuiteAggregateMethodTranslator( + GaussDBExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + /// + /// 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? Translate( + MethodInfo method, + EnumerableExpression source, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (source.Selector is not SqlExpression sqlExpression) + { + return null; + } + + if (method == ConvexHullMethod) + { + // PostGIS has no built-in aggregate convex hull, but we can simply apply ST_Collect beforehand as recommended in the docs + // https://postgis.net/docs/ST_ConvexHull.html + return _sqlExpressionFactory.Function( + "ST_ConvexHull", + [ + _sqlExpressionFactory.AggregateFunction( + "ST_Collect", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: [false], + typeof(Geometry), + GetMapping()) + ], + nullable: true, + argumentsPropagateNullability: [true], + typeof(Geometry), + GetMapping()); + } + + if (method == EnvelopeCombineMethod) + { + // ST_Extent returns a PostGIS box2d, which isn't a geometry and has no binary output function. + // Convert it to a geometry first. + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.AggregateFunction( + "ST_Extent", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: [false], + typeof(Geometry), + GetMapping()), + typeof(Geometry), GetMapping()); + } + + if (method == UnionMethod || method == GeometryCombineMethod) + { + return _sqlExpressionFactory.AggregateFunction( + method == UnionMethod ? "ST_Union" : "ST_Collect", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: [false], + typeof(Geometry), + GetMapping()); + } + + return null; + + RelationalTypeMapping? GetMapping() + => _typeMappingSource.FindMapping(typeof(Geometry), sqlExpression.TypeMapping?.StoreType ?? "geometry"); + } +} diff --git a/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteMemberTranslatorPlugin.cs b/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteMemberTranslatorPlugin.cs new file mode 100644 index 0000000000..5584979682 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteMemberTranslatorPlugin.cs @@ -0,0 +1,187 @@ +// ReSharper disable once CheckNamespace + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBNetTopologySuiteMemberTranslatorPlugin : IMemberTranslatorPlugin +{ + /// + /// 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 GaussDBNetTopologySuiteMemberTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + { + Translators = [new GaussDBGeometryMemberTranslator(sqlExpressionFactory, typeMappingSource)]; + } + + /// + /// 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 IEnumerable Translators { 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 class GaussDBGeometryMemberTranslator : IMemberTranslator +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly CaseWhenClause[] _ogcGeometryTypeWhenThenList; + + 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 + /// 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 GaussDBGeometryMemberTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + + _ogcGeometryTypeWhenThenList = + [ + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_CircularString"), _sqlExpressionFactory.Constant(OgcGeometryType.CircularString)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_CompoundCurve"), _sqlExpressionFactory.Constant(OgcGeometryType.CompoundCurve)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_CurvePolygon"), _sqlExpressionFactory.Constant(OgcGeometryType.CurvePolygon)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_GeometryCollection"), + _sqlExpressionFactory.Constant(OgcGeometryType.GeometryCollection)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_LineString"), _sqlExpressionFactory.Constant(OgcGeometryType.LineString)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_MultiCurve"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiCurve)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_MultiLineString"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiLineString)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_MultiPoint"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiPoint)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_MultiPolygon"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiPolygon)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_MultiSurface"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiSurface)), + new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Point"), _sqlExpressionFactory.Constant(OgcGeometryType.Point)), + new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Polygon"), _sqlExpressionFactory.Constant(OgcGeometryType.Polygon)), + new CaseWhenClause( + _sqlExpressionFactory.Constant("ST_PolyhedralSurface"), + _sqlExpressionFactory.Constant(OgcGeometryType.PolyhedralSurface)), + new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Tin"), _sqlExpressionFactory.Constant(OgcGeometryType.TIN)) + ]; + } + + /// + /// 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? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + var declaringType = member.DeclaringType; + + if (instance is null || !typeof(Geometry).IsAssignableFrom(declaringType)) + { + return null; + } + + var typeMapping = instance.TypeMapping; + Debug.Assert(typeMapping is not null, "Instance must have typeMapping assigned."); + var storeType = instance.TypeMapping!.StoreType; + + if (typeof(Point).IsAssignableFrom(declaringType)) + { + var function = member.Name switch + { + nameof(Point.X) => "ST_X", + nameof(Point.Y) => "ST_Y", + nameof(Point.Z) => "ST_Z", + nameof(Point.M) => "ST_M", + _ => null + }; + + if (function is not null) + { + return Function(function, [instance], typeof(double)); + } + } + + if (typeof(LineString).IsAssignableFrom(declaringType)) + { + if (member.Name == "Count") + { + return Function("ST_NumPoints", [instance], typeof(int)); + } + } + + return member.Name switch + { + 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", [instance], typeof(string)), + _ogcGeometryTypeWhenThenList, + elseResult: null), + + _ => null + }; + + SqlExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) + => _sqlExpressionFactory.Function( + name, arguments, + nullable: true, argumentsPropagateNullability: TrueArrays[arguments.Length], + returnType, typeMapping); + + RelationalTypeMapping ResultGeometryMapping() + { + Debug.Assert(typeof(Geometry).IsAssignableFrom(returnType)); + return _typeMappingSource.FindMapping(returnType, storeType)!; + } + } +} diff --git a/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteMethodCallTranslatorPlugin.cs b/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteMethodCallTranslatorPlugin.cs new file mode 100644 index 0000000000..8be22b911a --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Query/ExpressionTranslators/Internal/GaussDBNetTopologySuiteMethodCallTranslatorPlugin.cs @@ -0,0 +1,248 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBNetTopologySuiteMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin +{ + /// + /// 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 GaussDBNetTopologySuiteMethodCallTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + { + if (sqlExpressionFactory is not GaussDBExpressionFactory npgsqlSqlExpressionFactory) + { + throw new ArgumentException($"Must be an {nameof(GaussDBExpressionFactory)}", nameof(sqlExpressionFactory)); + } + + Translators = [new GaussDBGeometryMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource)]; + } + + /// + /// 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 IEnumerable Translators { 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 class GaussDBGeometryMethodTranslator : IMethodCallTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + + private static readonly bool[][] TrueArrays = + [ + [], [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 + /// 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 GaussDBGeometryMethodTranslator( + GaussDBExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => method.DeclaringType switch + { + var t when typeof(Geometry).IsAssignableFrom(t) && instance is not null + => TranslateGeometryMethod(instance, method, arguments), + + var t when t == typeof(GaussDBNetTopologySuiteDbFunctionsExtensions) + => TranslateDbFunction(method, arguments), + + // This handles the collection indexer (geom_collection[x] -> ST_GeometryN(geom_collection, x + 1)) + // This is needed as a special case because EF transforms the indexer into a call to Enumerable.ElementAt + var t when t == typeof(Enumerable) + && method.Name is nameof(Enumerable.ElementAt) + && method.ReturnType == typeof(Geometry) + && arguments is [var collection, var index] + && _typeMappingSource.FindMapping(typeof(Geometry), collection.TypeMapping!.StoreType) is RelationalTypeMapping geometryTypeMapping + => _sqlExpressionFactory.Function( + "ST_GeometryN", + [collection, OneBased(index)], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + geometryTypeMapping), + + _ => null + }; + + private SqlExpression? TranslateDbFunction( + MethodInfo method, + IReadOnlyList arguments) + => method.Name switch + { + nameof(GaussDBNetTopologySuiteDbFunctionsExtensions.Transform) => _sqlExpressionFactory.Function( + "ST_Transform", + [arguments[1], arguments[2]], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + arguments[1].TypeMapping), + + nameof(GaussDBNetTopologySuiteDbFunctionsExtensions.Force2D) => _sqlExpressionFactory.Function( + "ST_Force2D", + [arguments[1]], + nullable: true, + TrueArrays[1], + method.ReturnType, + arguments[1].TypeMapping), + + nameof(GaussDBNetTopologySuiteDbFunctionsExtensions.DistanceKnn) => _sqlExpressionFactory.MakePostgresBinary( + GaussDBExpressionType.Distance, + arguments[1], + arguments[2]), + + nameof(GaussDBNetTopologySuiteDbFunctionsExtensions.Distance) => + TranslateGeometryMethod(arguments[1], method, [arguments[2], arguments[3]]), + nameof(GaussDBNetTopologySuiteDbFunctionsExtensions.IsWithinDistance) => + TranslateGeometryMethod(arguments[1], method, [arguments[2], arguments[3], arguments[4]]), + + _ => null + }; + + private SqlExpression? TranslateGeometryMethod( + SqlExpression instance, + MethodInfo method, + IReadOnlyList arguments) + { + var typeMapping = ExpressionExtensions.InferTypeMapping( + arguments.Prepend(instance).Where(e => typeof(Geometry).IsAssignableFrom(e.Type)).ToArray()); + + Debug.Assert(typeMapping is not null, "At least one argument must have typeMapping."); + var storeType = typeMapping.StoreType; + + instance = _sqlExpressionFactory.ApplyTypeMapping(instance, _typeMappingSource.FindMapping(instance.Type, storeType)); + + var typeMappedArguments = new List(); + foreach (var argument in arguments) + { + typeMappedArguments.Add( + _sqlExpressionFactory.ApplyTypeMapping( + argument, + typeof(Geometry).IsAssignableFrom(argument.Type) + ? _typeMappingSource.FindMapping(argument.Type, storeType) + : _typeMappingSource.FindMapping(argument.Type))); + } + + arguments = typeMappedArguments; + + return method.Name switch + { + nameof(Geometry.AsBinary) + => Function("ST_AsBinary", [instance], typeof(byte[])), + nameof(Geometry.AsText) + => 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", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.ConvexHull) + => Function("ST_ConvexHull", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.CoveredBy) + => Function("ST_CoveredBy", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Covers) + => Function("ST_Covers", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Crosses) + => Function("ST_Crosses", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Disjoint) + => Function("ST_Disjoint", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Difference) + => 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", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.EqualsTopologically) + => Function("ST_Equals", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.GetGeometryN) + => Function("ST_GeometryN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), + nameof(Polygon.GetInteriorRingN) + => Function("ST_InteriorRingN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), + nameof(LineString.GetPointN) + => Function("ST_PointN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.Intersection) + => Function("ST_Intersection", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.Intersects) + => 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", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.Overlaps) + => Function("ST_Overlaps", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Relate) + => Function("ST_Relate", [instance, arguments[0], arguments[1]], typeof(bool)), + nameof(Geometry.Reverse) + => Function("ST_Reverse", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.SymmetricDifference) + => Function("ST_SymDifference", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.ToBinary) + => Function("ST_AsBinary", [instance], typeof(byte[])), + nameof(Geometry.ToText) + => Function("ST_AsText", [instance], typeof(string)), + nameof(Geometry.Touches) + => Function("ST_Touches", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Within) + => Function("ST_Within", [instance, arguments[0]], typeof(bool)), + nameof(Geometry.Union) when arguments.Count == 0 + => Function("ST_UnaryUnion", [instance], typeof(Geometry), ResultGeometryMapping()), + nameof(Geometry.Union) when arguments.Count == 1 + => Function("ST_Union", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), + + _ => null + }; + + SqlExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) + => _sqlExpressionFactory.Function( + name, arguments, + nullable: true, argumentsPropagateNullability: TrueArrays[arguments.Length], + returnType, typeMapping); + + RelationalTypeMapping ResultGeometryMapping() + { + Debug.Assert(typeof(Geometry).IsAssignableFrom(method.ReturnType)); + return _typeMappingSource.FindMapping(method.ReturnType, storeType)!; + } + } + + // NetTopologySuite uses 0-based indexing, but PostGIS uses 1-based + private SqlExpression OneBased(SqlExpression arg) + => arg is SqlConstantExpression constant + ? _sqlExpressionFactory.Constant((int)constant.Value! + 1, constant.TypeMapping) + : _sqlExpressionFactory.Add(arg, _sqlExpressionFactory.Constant(1)); +} diff --git a/src/EFCore.GaussDB.NTS/README.md b/src/EFCore.GaussDB.NTS/README.md new file mode 100644 index 0000000000..8a641dc96e --- /dev/null +++ b/src/EFCore.GaussDB.NTS/README.md @@ -0,0 +1,43 @@ +# GaussDB Entity Framework Core provider for GaussDB + +HuaweiCloud.EntityFrameworkCore.GaussDB is the open source EF Core provider for GaussDB. It allows you to interact with GaussDB via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. + +This package is a plugin which allows you to interact with spatial data provided by the GaussDB [PostGIS extension](https://postgis.net); PostGIS is a mature, standard extension considered to provide top-of-the-line database spatial features. On the .NET side, the plugin adds support for the types from the [NetTopologySuite library](https://github.com/NetTopologySuite/NetTopologySuite), allowing you to read and write them directly to GaussDB. + +To use the plugin, simply add `UseNetTopologySuite` as below and use NetTopologySuite types in your entity properties: + +```csharp +await using var ctx = new BlogContext(); +await ctx.Database.EnsureDeletedAsync(); +await ctx.Database.EnsureCreatedAsync(); + +// Insert a Blog +ctx.Cities.Add(new() +{ + Name = "FooCity", + Center = new Point(10, 10) +}); +await ctx.SaveChangesAsync(); + +// Query all cities with the given center point +var newBlogs = await ctx.Cities.Where(b => b.Center == new Point(10, 10)).ToListAsync(); + +public class BlogContext : DbContext +{ + public DbSet Cities { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB( + @"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase", + o => o.UseNetTopologySuite()); +} + +public class City +{ + public int Id { get; set; } + public string Name { get; set; } + public Point Center { get; set; } +} +``` + +The plugin also supports translating many NetTopologySuite methods and properties into corresponding PostGIS operations. For more information, see the [NetTopologySuite plugin documentation page](https://www.npgsql.org/efcore/mapping/nts.html). diff --git a/src/EFCore.GaussDB.NTS/Scaffolding/Internal/GaussDBNetTopologySuiteCodeGeneratorPlugin.cs b/src/EFCore.GaussDB.NTS/Scaffolding/Internal/GaussDBNetTopologySuiteCodeGeneratorPlugin.cs new file mode 100644 index 0000000000..cac7708191 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Scaffolding/Internal/GaussDBNetTopologySuiteCodeGeneratorPlugin.cs @@ -0,0 +1,30 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite.Scaffolding.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 GaussDBNetTopologySuiteCodeGeneratorPlugin : ProviderCodeGeneratorPlugin +{ + private static readonly MethodInfo _useNetTopologySuiteMethodInfo + = typeof(GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite), + typeof(GaussDBDbContextOptionsBuilder), + typeof(CoordinateSequenceFactory), + typeof(PrecisionModel), + typeof(Ordinates), + typeof(bool)); + + /// + /// 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 MethodCallCodeFragment GenerateProviderOptions() + => new(_useNetTopologySuiteMethodInfo); +} diff --git a/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBGeometryTypeMapping.cs b/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBGeometryTypeMapping.cs new file mode 100644 index 0000000000..b3e8c5996b --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBGeometryTypeMapping.cs @@ -0,0 +1,126 @@ +using System.Data.Common; +using System.Text; +using JetBrains.Annotations; +using NetTopologySuite.IO; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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. +/// +[UsedImplicitly] +public class GaussDBGeometryTypeMapping : RelationalGeometryTypeMapping, IGaussDBTypeMapping +{ + private readonly bool _isGeography; + + /// + public virtual GaussDBDbType GaussDBDbType + => _isGeography ? GaussDBDbType.Geography : GaussDBDbType.Geometry; + + /// + /// 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 GaussDBGeometryTypeMapping(string storeType, bool isGeography) + : base(converter: null, storeType, GaussDBJsonGeometryWktReaderWriter.Instance) + { + _isGeography = isGeography; + } + + /// + /// 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 GaussDBGeometryTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, converter: 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 override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBGeometryTypeMapping(parameters); + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + base.ConfigureParameter(parameter); + + ((GaussDBParameter)parameter).GaussDBDbType = GaussDBDbType; + } + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var geometry = (Geometry)value; + var builder = new StringBuilder(); + + builder + .Append(_isGeography ? "GEOGRAPHY" : "GEOMETRY") + .Append(" '"); + + if (geometry.SRID > 0) + { + builder + .Append("SRID=") + .Append(geometry.SRID) + .Append(';'); + } + + builder + .Append(geometry.AsText()) + .Append('\''); + + return builder.ToString(); + } + + /// + /// 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 AsText(object value) + => ((Geometry)value).AsText(); + + /// + /// 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 int GetSrid(object value) + => ((Geometry)value).SRID; + + /// + /// 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 Type WktReaderType + => typeof(WKTReader); +} diff --git a/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBJsonGeometryWktReaderWriter.cs b/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBJsonGeometryWktReaderWriter.cs new file mode 100644 index 0000000000..e1f9348aa8 --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBJsonGeometryWktReaderWriter.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; +using NetTopologySuite.IO; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +/// +/// Reads and writes JSON using the well-known-text format for values. +/// +public sealed class GaussDBJsonGeometryWktReaderWriter : JsonValueReaderWriter +{ + private static readonly PropertyInfo InstanceProperty = typeof(GaussDBJsonGeometryWktReaderWriter).GetProperty(nameof(Instance))!; + + private static readonly WKTReader WktReader = new(); + + /// + /// The singleton instance of this stateless reader/writer. + /// + public static GaussDBJsonGeometryWktReaderWriter Instance { get; } = new(); + + private GaussDBJsonGeometryWktReaderWriter() + { + } + + /// + public override Geometry FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => WktReader.Read(manager.CurrentReader.GetString()); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) + { + 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 GaussDB 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.GaussDB.NTS/Storage/Internal/GaussDBNetTopologySuiteTypeMappingSourcePlugin.cs b/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBNetTopologySuiteTypeMappingSourcePlugin.cs new file mode 100644 index 0000000000..aae1421f8e --- /dev/null +++ b/src/EFCore.GaussDB.NTS/Storage/Internal/GaussDBNetTopologySuiteTypeMappingSourcePlugin.cs @@ -0,0 +1,204 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBNetTopologySuiteTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin +{ + // Note: we reference the options rather than copying IsGeographyDefault out, because that field is initialized + // rather late by SingletonOptionsInitializer + private readonly IGaussDBNetTopologySuiteSingletonOptions _options; + + private static bool TryGetClrType(string subtypeName, [NotNullWhen(true)] out Type? clrType) + { + clrType = subtypeName switch + { + "POINT" => typeof(Point), + "LINESTRING" => typeof(LineString), + "POLYGON" => typeof(Polygon), + "MULTIPOINT" => typeof(MultiPoint), + "MULTILINESTRING" => typeof(MultiLineString), + "MULTIPOLYGON" => typeof(MultiPolygon), + "GEOMETRYCOLLECTION" => typeof(GeometryCollection), + "GEOMETRY" => typeof(Geometry), + _ => null + }; + + return clrType is not 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 GaussDBNetTopologySuiteTypeMappingSourcePlugin(IGaussDBNetTopologySuiteSingletonOptions options) + { + _options = Check.NotNull(options, nameof(options)); + } + + /// + /// 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 RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) + { + // TODO: Array + var clrType = mappingInfo.ClrType; + var storeTypeName = mappingInfo.StoreTypeName; + var isGeography = _options.IsGeographyDefault; + + if (clrType is not null) + { + if (!clrType.IsAssignableTo(typeof(Geometry))) + { + return null; + } + + // TODO: if store type is null, consider setting it based on the CLR type, i.e. create GEOMETRY(Point) instead of Geometry when + // the CLR property is NTS Point. + } + + if (storeTypeName is not null) + { + if (!TryParseStoreTypeName(storeTypeName, out _, out isGeography, out var parsedSubtype, out _, out _)) + { + return null; + } + + clrType ??= parsedSubtype; + } + + storeTypeName ??= isGeography ? "geography" : "geometry"; + + Check.DebugAssert(clrType is not null, "clrType is not null"); + + var typeMapping = (RelationalTypeMapping)Activator.CreateInstance( + typeof(GaussDBGeometryTypeMapping<>).MakeGenericType(clrType), storeTypeName, isGeography)!; + + // TODO: for geometry collection support (and why the following is commented out), see #2850. + + // // TODO: Also restrict the element type mapping based on the user-specified store type? + // var elementType = clrType == typeof(MultiPoint) + // ? typeof(Point) + // : clrType == typeof(MultiLineString) + // ? typeof(LineString) + // : clrType == typeof(MultiPolygon) + // ? typeof(Polygon) + // : clrType == typeof(GeometryCollection) + // ? typeof(Geometry) + // : null; + // + // if (elementType is not null) + // { + // var elementTypeMapping = FindMapping(new() { ClrType = elementType })!; + // + // typeMapping = typeMapping.Clone(elementMapping: elementTypeMapping); + // } + + return typeMapping; + } + + /// + /// Given a PostGIS store type name (e.g. GEOMETRY, GEOGRAPHY(Point, 4326), GEOMETRY(LineStringM, 4326)), + /// attempts to parse it and return its components. + /// + public static bool TryParseStoreTypeName( + string storeTypeName, + out string subtypeName, + out bool isGeography, + out Type? clrType, + out int srid, + out Ordinates ordinates) + { + storeTypeName = storeTypeName.Trim(); + subtypeName = storeTypeName; + isGeography = false; + clrType = typeof(Geometry); + srid = -1; + ordinates = Ordinates.AllOrdinates; + + var openParen = storeTypeName.IndexOf("(", StringComparison.Ordinal); + + var baseType = openParen > 0 ? storeTypeName.Substring(0, openParen).Trim() : storeTypeName; + + if (baseType.Equals("GEOMETRY", StringComparison.OrdinalIgnoreCase)) + { + isGeography = false; + } + else if (baseType.Equals("GEOGRAPHY", StringComparison.OrdinalIgnoreCase)) + { + isGeography = true; + } + else + { + return false; + } + + if (openParen == -1) + { + return true; + } + + var closeParen = storeTypeName.IndexOf(")", openParen + 1, StringComparison.Ordinal); + if (closeParen != storeTypeName.Length - 1) + { + return false; + } + + var comma = storeTypeName.IndexOf(",", openParen + 1, StringComparison.Ordinal); + if (comma == -1) + { + subtypeName = storeTypeName.Substring(openParen + 1, closeParen - openParen - 1).Trim(); + } + else + { + subtypeName = storeTypeName.Substring(openParen + 1, comma - openParen - 1).Trim(); + + if (!int.TryParse(storeTypeName.Substring(comma + 1, closeParen - comma - 1).Trim(), out srid)) + { + return false; + } + } + + subtypeName = subtypeName.ToUpper(CultureInfo.InvariantCulture); + + // We have geometry(subtype, srid), parse the subtype (POINT, POINTZ, POINTM, POINTZM...) + + if (TryGetClrType(subtypeName, out clrType)) + { + return true; + } + + if (subtypeName.EndsWith("ZM", StringComparison.Ordinal) && TryGetClrType(subtypeName[..^2], out clrType)) + { + ordinates = Ordinates.XYZM; + return true; + } + + if (subtypeName.EndsWith("M", StringComparison.Ordinal) && TryGetClrType(subtypeName[..^1], out clrType)) + { + ordinates = Ordinates.XYM; + return true; + } + + if (subtypeName.EndsWith("Z", StringComparison.Ordinal) && TryGetClrType(subtypeName[..^1], out clrType)) + { + ordinates = Ordinates.XYZ; + return true; + } + + return false; + } +} diff --git a/src/EFCore.GaussDB.NTS/build/netstandard2.0/HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite.targets b/src/EFCore.GaussDB.NTS/build/netstandard2.0/HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite.targets new file mode 100644 index 0000000000..742cb518ea --- /dev/null +++ b/src/EFCore.GaussDB.NTS/build/netstandard2.0/HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite.targets @@ -0,0 +1,46 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + $(IntermediateOutputPath)EFCoreGaussDBNetTopologySuite$(DefaultLanguageSourceExtension) + + + + + + + CompileBefore + + + + + CompileAfter + + + + + + + Compile + + + + + + + <_Parameter1>HuaweiCloud.EntityFrameworkCore.GaussDB.Design.Internal.GaussDBNetTopologySuiteDesignTimeServices, HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite + <_Parameter2>HuaweiCloud.EntityFrameworkCore.GaussDB + + + + + + + + diff --git a/src/EFCore.GaussDB.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs b/src/EFCore.GaussDB.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs new file mode 100644 index 0000000000..70000bf60e --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs @@ -0,0 +1,27 @@ +using JetBrains.Annotations; +using HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Design.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. +/// +[UsedImplicitly] +public class GaussDBNodaTimeDesignTimeServices : IDesignTimeServices +{ + /// + /// 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 void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + => serviceCollection + .AddSingleton() + .AddSingleton(); +} diff --git a/src/EFCore.GaussDB.NodaTime/EFCore.GaussDB.NodaTime.csproj b/src/EFCore.GaussDB.NodaTime/EFCore.GaussDB.NodaTime.csproj new file mode 100644 index 0000000000..abe6e67928 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/EFCore.GaussDB.NodaTime.csproj @@ -0,0 +1,33 @@ + + + HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime + HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime + + Conan Yao + NodaTime support plugin for GaussDB Entity Framework Core provider. + gauss;gaussdb;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;nodatime;date;time + README.md + + + + + + + + + + + + + + + + + + + True + build + + + + diff --git a/src/EFCore.GaussDB.NodaTime/EFCore.GaussDB.NodaTime.csproj.Backup.tmp b/src/EFCore.GaussDB.NodaTime/EFCore.GaussDB.NodaTime.csproj.Backup.tmp new file mode 100644 index 0000000000..db15a12deb --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/EFCore.GaussDB.NodaTime.csproj.Backup.tmp @@ -0,0 +1,33 @@ + + + HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime + HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime + + Shay Rojansky + NodaTime support plugin for GaussDB Entity Framework Core provider. + npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;nodatime;date;time + README.md + + + + + + + + + + + + + + + + + + + True + build + + + + diff --git a/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeDbContextOptionsBuilderExtensions.cs b/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeDbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000000..d7a503e207 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,30 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// NodaTime specific extension methods for . +/// +public static class GaussDBNodaTimeDbContextOptionsBuilderExtensions +{ + /// + /// Configure NodaTime type mappings for Entity Framework. + /// + /// The options builder so that further configuration can be chained. + public static GaussDBDbContextOptionsBuilder UseNodaTime( + this GaussDBDbContextOptionsBuilder optionsBuilder) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; + + var extension = coreOptionsBuilder.Options.FindExtension() + ?? new GaussDBNodaTimeOptionsExtension(); + + ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } +} diff --git a/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeDbFunctionsExtensions.cs b/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeDbFunctionsExtensions.cs new file mode 100644 index 0000000000..2fea658776 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeDbFunctionsExtensions.cs @@ -0,0 +1,189 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods supporting NodaTime function translation for GaussDB. +/// +public static class GaussDBNodaTimeDbFunctionsExtensions +{ + /// + /// Computes the sum of the non-null input intervals. Corresponds to the GaussDB sum aggregate function. + /// + /// The instance. + /// The input values to be summed. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static Period? Sum(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Sum))); + + /// + /// Computes the sum of the non-null input intervals. Corresponds to the GaussDB sum aggregate function. + /// + /// The instance. + /// The input values to be summed. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static Duration? Sum(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Sum))); + + /// + /// Computes the average (arithmetic mean) of the non-null input intervals. Corresponds to the GaussDB avg aggregate function. + /// + /// The instance. + /// The input values to be computed into an average. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static Period? Average(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); + + /// + /// Computes the average (arithmetic mean) of the non-null input intervals. Corresponds to the GaussDB avg aggregate function. + /// + /// The instance. + /// The input values to be computed into an average. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static Duration? Average(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); + + /// + /// Returns the distance between two instants as a , particularly suitable for sorting where the appropriate index + /// is defined. + /// + /// + /// This requires the btree_gist built-in GaussDB extension, see + /// . + /// + public static int Distance(this DbFunctions _, Instant a, Instant b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Returns the distance between two zoned timestamps as a , particularly suitable for sorting where the + /// appropriate index is defined. + /// + /// + /// This requires the btree_gist built-in GaussDB extension, see + /// . + /// + public static int Distance(this DbFunctions _, ZonedDateTime a, ZonedDateTime b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Returns the distance between two local timestamps as a , particularly suitable for sorting where the + /// appropriate index is defined. + /// + /// + /// This requires the btree_gist built-in GaussDB extension, see + /// . + /// + public static int Distance(this DbFunctions _, LocalDateTime a, LocalDateTime b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Returns the distance between two dates as a number of days, particularly suitable for sorting where the appropriate index is + /// defined. + /// + /// + /// This requires the btree_gist built-in GaussDB extension, see + /// . + /// + public static int Distance(this DbFunctions _, LocalDate a, LocalDate b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + #region Aggregate functions + + /// + /// Computes the union of the non-null input intervals. Corresponds to the GaussDB range_agg aggregate function. + /// + /// The instance. + /// The intervals to be aggregated via union into a multirange. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static Interval[] RangeAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeAgg))); + + /// + /// Computes the union of the non-null input date intervals. Corresponds to the GaussDB range_agg aggregate function. + /// + /// The instance. + /// The date intervals to be aggregated via union into a multirange. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static DateInterval[] RangeAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeAgg))); + + /// + /// Computes the intersection of the non-null input intervals. Corresponds to the GaussDB range_intersect_agg aggregate function. + /// + /// The instance. + /// The intervals on which to perform the intersection operation. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static Interval RangeIntersectAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); + + /// + /// Computes the intersection of the non-null input date intervals. Corresponds to the GaussDB range_intersect_agg aggregate + /// function. + /// + /// The instance. + /// The date intervals on which to perform the intersection operation. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static DateInterval RangeIntersectAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); + + /// + /// Computes the intersection of the non-null input interval multiranges. + /// Corresponds to the GaussDB range_intersect_agg aggregate function. + /// + /// The instance. + /// The intervals on which to perform the intersection operation. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static Interval[] RangeIntersectAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); + + /// + /// Computes the intersection of the non-null input date interval multiranges. + /// Corresponds to the GaussDB range_intersect_agg aggregate function. + /// + /// The instance. + /// The date intervals on which to perform the intersection operation. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of + /// an EF Core LINQ query. + /// + public static DateInterval[] RangeIntersectAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); + + #endregion Aggregate functions +} diff --git a/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeServiceCollectionExtensions.cs b/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeServiceCollectionExtensions.cs new file mode 100644 index 0000000000..a59879f891 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Extensions/GaussDBNodaTimeServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Query.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime extension methods for . +/// +public static class GaussDBNodaTimeServiceCollectionExtensions +{ + /// + /// Adds the services required for NodaTime support in the GaussDB provider for Entity Framework. + /// + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddEntityFrameworkGaussDBNodaTime( + this IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + new EntityFrameworkGaussDBServicesBuilder(serviceCollection) + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd(); + + return serviceCollection; + } +} diff --git a/src/EFCore.GaussDB.NodaTime/Extensions/TypeExtensions.cs b/src/EFCore.GaussDB.NodaTime/Extensions/TypeExtensions.cs new file mode 100644 index 0000000000..c99010cac3 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Extensions/TypeExtensions.cs @@ -0,0 +1,22 @@ +// ReSharper disable once CheckNamespace + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +internal static class TypeExtensions +{ + internal static bool IsGenericList(this Type type) + => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + + internal static bool IsArrayOrGenericList(this Type type) + => type.IsArray || type.IsGenericList(); + + internal static bool TryGetElementType(this Type type, out Type? elementType) + { + elementType = type.IsArray + ? type.GetElementType() + : type.IsGenericList() + ? type.GetGenericArguments()[0] + : null; + return elementType is not null; + } +} diff --git a/src/EFCore.GaussDB.NodaTime/Infrastructure/Internal/GaussDBNodaTimeOptionsExtension.cs b/src/EFCore.GaussDB.NodaTime/Infrastructure/Internal/GaussDBNodaTimeOptionsExtension.cs new file mode 100644 index 0000000000..9676d204cd --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Infrastructure/Internal/GaussDBNodaTimeOptionsExtension.cs @@ -0,0 +1,78 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBNodaTimeOptionsExtension : 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 void ApplyServices(IServiceCollection services) + => services.AddEntityFrameworkGaussDBNodaTime(); + + /// + /// 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 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 void Validate(IDbContextOptions options) + { + var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; + if (internalServiceProvider is not null) + { + using (var scope = internalServiceProvider.CreateScope()) + { + if (scope.ServiceProvider.GetService>() + ?.Any(s => s is GaussDBNodaTimeTypeMappingSourcePlugin) + != true) + { + throw new InvalidOperationException( + $"{nameof(GaussDBNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime)} requires {nameof(GaussDBNodaTimeServiceCollectionExtensions.AddEntityFrameworkGaussDBNodaTime)} to be called on the internal service provider used."); + } + } + } + } + + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) + { + private new GaussDBNodaTimeOptionsExtension Extension + => (GaussDBNodaTimeOptionsExtension)base.Extension; + + public override bool IsDatabaseProvider + => false; + + public override int GetServiceProviderHashCode() + => 0; + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => true; + + public override void PopulateDebugInfo(IDictionary debugInfo) + => debugInfo["GaussDB:" + nameof(GaussDBNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime)] = "1"; + + public override string LogFragment + => "using NodaTime "; + } +} diff --git a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs b/src/EFCore.GaussDB.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs similarity index 80% rename from src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs rename to src/EFCore.GaussDB.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs index b6fb4e58d9..33648fbf79 100644 --- a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs +++ b/src/EFCore.GaussDB.NodaTime/Infrastructure/Internal/NodaTimeDataSourceConfigurationPlugin.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -6,7 +6,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.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 NodaTimeDataSourceConfigurationPlugin : INpgsqlDataSourceConfigurationPlugin +public class NodaTimeDataSourceConfigurationPlugin : IGaussDBDataSourceConfigurationPlugin { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -14,6 +14,6 @@ public class NodaTimeDataSourceConfigurationPlugin : INpgsqlDataSourceConfigurat /// 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) + public void Configure(GaussDBDataSourceBuilder npgsqlDataSourceBuilder) => npgsqlDataSourceBuilder.UseNodaTime(); } diff --git a/src/EFCore.GaussDB.NodaTime/Properties/AssemblyInfo.cs b/src/EFCore.GaussDB.NodaTime/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..476c7a9b4a --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; + +[assembly: + InternalsVisibleTo( + "HuaweiCloud.EntityFrameworkCore.GaussDB.FunctionalTests, PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100" + + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" + + "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" + + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] diff --git a/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeAggregateMethodCallTranslatorPlugin.cs new file mode 100644 index 0000000000..683079a12f --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeAggregateMethodCallTranslatorPlugin.cs @@ -0,0 +1,108 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.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 GaussDBNodaTimeAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin +{ + /// + /// 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 GaussDBNodaTimeAggregateMethodCallTranslatorPlugin( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + if (sqlExpressionFactory is not GaussDBExpressionFactory npgsqlSqlExpressionFactory) + { + throw new ArgumentException($"Must be an {nameof(GaussDBExpressionFactory)}", nameof(sqlExpressionFactory)); + } + + Translators = + [ + new GaussDBNodaTimeAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) + ]; + } + + /// + /// 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 IEnumerable Translators { 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 class GaussDBNodaTimeAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private static readonly bool[][] FalseArrays = [[], [false]]; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + + /// + /// 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 GaussDBNodaTimeAggregateMethodTranslator( + GaussDBExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + /// + /// 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? Translate( + MethodInfo method, + EnumerableExpression source, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (source.Selector is not SqlExpression sqlExpression || method.DeclaringType != typeof(GaussDBNodaTimeDbFunctionsExtensions)) + { + return null; + } + + return method.Name switch + { + nameof(GaussDBNodaTimeDbFunctionsExtensions.Sum) => _sqlExpressionFactory.AggregateFunction( + "sum", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, sqlExpression.TypeMapping), + + nameof(GaussDBNodaTimeDbFunctionsExtensions.Average) => _sqlExpressionFactory.AggregateFunction( + "avg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, sqlExpression.TypeMapping), + + nameof(GaussDBNodaTimeDbFunctionsExtensions.RangeAgg) => _sqlExpressionFactory.AggregateFunction( + "range_agg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + returnType: method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType)), + + nameof(GaussDBNodaTimeDbFunctionsExtensions.RangeIntersectAgg) => _sqlExpressionFactory.AggregateFunction( + "range_intersect_agg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, sqlExpression.TypeMapping), + + _ => null + }; + } +} diff --git a/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeEvaluatableExpressionFilterPlugin.cs b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeEvaluatableExpressionFilterPlugin.cs new file mode 100644 index 0000000000..bd28001259 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeEvaluatableExpressionFilterPlugin.cs @@ -0,0 +1,50 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.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 GaussDBNodaTimeEvaluatableExpressionFilterPlugin : IEvaluatableExpressionFilterPlugin +{ + private static readonly MethodInfo GetCurrentInstantMethod = + typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), [])!; + + private static readonly MemberInfo SystemClockInstanceMember = + typeof(SystemClock).GetMember(nameof(SystemClock.Instance)).FirstOrDefault()!; + + /// + /// 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 bool IsEvaluatableExpression(Expression expression) + { + switch (expression) + { + case MethodCallExpression methodCallExpression when methodCallExpression.Method == GetCurrentInstantMethod: + return false; + + case MemberExpression memberExpression: + if (memberExpression.Member == SystemClockInstanceMember) + { + return false; + } + + // We support translating certain NodaTime patterns which accept a time zone as a parameter, + // e.g. Instant.InZone(timezone), as long as the timezone is expressed as an access on DateTimeZoneProviders.Tzdb. + // Prevent this from being evaluated locally and so parameterized, so we can access the member access on + // DateTimeZoneProviders and extract the constant (see GaussDBNodaTimeMethodCallTranslatorPlugin) + if (memberExpression.Member.DeclaringType == typeof(DateTimeZoneProviders)) + { + return false; + } + + break; + } + + return true; + } +} diff --git a/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeMemberTranslatorPlugin.cs b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeMemberTranslatorPlugin.cs new file mode 100644 index 0000000000..721cbdc1c4 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeMemberTranslatorPlugin.cs @@ -0,0 +1,395 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Query.Internal; + +/// +/// Provides translation services for members. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-datetime.html +/// +public class GaussDBNodaTimeMemberTranslatorPlugin : IMemberTranslatorPlugin +{ + /// + /// 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 GaussDBNodaTimeMemberTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + { + Translators = + [ + new GaussDBNodaTimeMemberTranslator(typeMappingSource, (GaussDBExpressionFactory)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 virtual IEnumerable Translators { 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 class GaussDBNodaTimeMemberTranslator : IMemberTranslator +{ + private static readonly MemberInfo SystemClock_Instance = + typeof(SystemClock).GetRuntimeProperty(nameof(SystemClock.Instance))!; + + private static readonly MemberInfo ZonedDateTime_LocalDateTime = + typeof(ZonedDateTime).GetRuntimeProperty(nameof(ZonedDateTime.LocalDateTime))!; + + private static readonly MemberInfo Interval_Start = + typeof(Interval).GetRuntimeProperty(nameof(Interval.Start))!; + + private static readonly MemberInfo Interval_End = + typeof(Interval).GetRuntimeProperty(nameof(Interval.End))!; + + private static readonly MemberInfo Interval_HasStart = + typeof(Interval).GetRuntimeProperty(nameof(Interval.HasStart))!; + + private static readonly MemberInfo Interval_HasEnd = + typeof(Interval).GetRuntimeProperty(nameof(Interval.HasEnd))!; + + private static readonly MemberInfo Interval_Duration = + typeof(Interval).GetRuntimeProperty(nameof(Interval.Duration))!; + + private static readonly MemberInfo DateInterval_Start = + typeof(DateInterval).GetRuntimeProperty(nameof(DateInterval.Start))!; + + private static readonly MemberInfo DateInterval_End = + typeof(DateInterval).GetRuntimeProperty(nameof(DateInterval.End))!; + + private static readonly MemberInfo DateInterval_Length = + typeof(DateInterval).GetRuntimeProperty(nameof(DateInterval.Length))!; + + private static readonly MemberInfo DateTimeZoneProviders_TzDb = + typeof(DateTimeZoneProviders).GetRuntimeProperty(nameof(DateTimeZoneProviders.Tzdb))!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly RelationalTypeMapping _dateTypeMapping; + private readonly RelationalTypeMapping _periodTypeMapping; + private readonly RelationalTypeMapping _localDateTimeTypeMapping; + + /// + /// 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 GaussDBNodaTimeMemberTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _dateTypeMapping = typeMappingSource.FindMapping(typeof(LocalDate))!; + _periodTypeMapping = typeMappingSource.FindMapping(typeof(Period))!; + _localDateTimeTypeMapping = typeMappingSource.FindMapping(typeof(LocalDateTime))!; + } + + private static readonly bool[][] TrueArrays = [[], [true], [true, true]]; + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + // This is necessary to allow translation of methods on SystemClock.Instance + if (member == SystemClock_Instance) + { + return _sqlExpressionFactory.Constant(SystemClock.Instance); + } + + if (member == DateTimeZoneProviders_TzDb) + { + return PendingDateTimeZoneProviderExpression.Instance; + } + + if (instance is null) + { + return null; + } + + var declaringType = member.DeclaringType; + + if (declaringType == typeof(LocalDateTime) + || declaringType == typeof(LocalDate) + || declaringType == typeof(LocalTime) + || declaringType == typeof(Period)) + { + return TranslateDateTime(instance, member); + } + + if (declaringType == typeof(ZonedDateTime)) + { + return TranslateZonedDateTime(instance, member, returnType); + } + + if (declaringType == typeof(Duration)) + { + return TranslateDuration(instance, member); + } + + if (declaringType == typeof(Interval)) + { + return TranslateInterval(instance, member); + } + + if (declaringType == typeof(DateInterval)) + { + return TranslateDateInterval(instance, member); + } + + return null; + } + + private SqlExpression? TranslateDuration(SqlExpression instance, MemberInfo member) + { + return member.Name switch + { + nameof(Duration.TotalDays) => TranslateDurationTotalMember(instance, 86400), + nameof(Duration.TotalHours) => TranslateDurationTotalMember(instance, 3600), + nameof(Duration.TotalMinutes) => TranslateDurationTotalMember(instance, 60), + nameof(Duration.TotalSeconds) => GetDatePartExpressionDouble(instance, "epoch"), + nameof(Duration.TotalMilliseconds) => TranslateDurationTotalMember(instance, 0.001), + nameof(Duration.Days) => GetDatePartExpression(instance, "day"), + nameof(Duration.Hours) => GetDatePartExpression(instance, "hour"), + nameof(Duration.Minutes) => GetDatePartExpression(instance, "minute"), + nameof(Duration.Seconds) => GetDatePartExpression(instance, "second", true), + nameof(Duration.Milliseconds) => null, // Too annoying, floating point and sub-millisecond handling + _ => null, + }; + + SqlExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) + => _sqlExpressionFactory.Divide(GetDatePartExpressionDouble(instance, "epoch"), _sqlExpressionFactory.Constant(divisor)); + } + + private SqlExpression? TranslateInterval(SqlExpression instance, MemberInfo member) + { + if (member == Interval_Start) + { + return Lower(); + } + + if (member == Interval_End) + { + return Upper(); + } + + if (member == Interval_HasStart) + { + return _sqlExpressionFactory.Not( + _sqlExpressionFactory.Function( + "lower_inf", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(bool))); + } + + if (member == Interval_HasEnd) + { + return _sqlExpressionFactory.Not( + _sqlExpressionFactory.Function( + "upper_inf", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(bool))); + } + + if (member == Interval_Duration) + { + return _sqlExpressionFactory.Subtract(Upper(), Lower(), _typeMappingSource.FindMapping(typeof(Duration))); + } + + return null; + + SqlExpression Lower() + => _sqlExpressionFactory.Function( + "lower", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(Interval), + _typeMappingSource.FindMapping(typeof(Instant))); + + SqlExpression Upper() + => _sqlExpressionFactory.Function( + "upper", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(Interval), + _typeMappingSource.FindMapping(typeof(Instant))); + } + + private SqlExpression? TranslateDateInterval(SqlExpression instance, MemberInfo member) + { + // NodaTime DateInterval is inclusive on both ends. + // GaussDB daterange is a discrete range type; this means it gets normalized to inclusive lower bound, exclusive upper bound. + // So we can translate Start as-is, but need to subtract a day for End. + if (member == DateInterval_Start) + { + return Lower(); + } + + if (member == DateInterval_End) + { + // GaussDB 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)), typeof(LocalDate), + _typeMappingSource.FindMapping(typeof(LocalDate))); + } + + if (member == DateInterval_Length) + { + return _sqlExpressionFactory.Subtract(Upper(), Lower()); + } + + return null; + + SqlExpression Lower() + => _sqlExpressionFactory.Function( + "lower", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(LocalDate), + _dateTypeMapping); + + SqlExpression Upper() + => _sqlExpressionFactory.Function( + "upper", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(LocalDate), + _dateTypeMapping); + } + + private SqlExpression? TranslateDateTime(SqlExpression instance, MemberInfo member) + => member.Name switch + { + "Year" or "Years" => GetDatePartExpression(instance, "year"), + "Month" or "Months" => GetDatePartExpression(instance, "month"), + "DayOfYear" => GetDatePartExpression(instance, "doy"), + "Day" or "Days" => GetDatePartExpression(instance, "day"), + "Hour" or "Hours" => GetDatePartExpression(instance, "hour"), + "Minute" or "Minutes" => GetDatePartExpression(instance, "minute"), + "Second" or "Seconds" => GetDatePartExpression(instance, "second", true), + "Millisecond" or "Milliseconds" => null, // Too annoying + + // Unlike DateTime.DayOfWeek, NodaTime's IsoDayOfWeek enum doesn't exactly correspond to GaussDB's + // values returned by date_part('dow', ...): in NodaTime Sunday is 7 and not 0, which is None. + // So we generate a CASE WHEN expression to translate GaussDB's 0 to 7. + "DayOfWeek" when GetDatePartExpression(instance, "dow", true) is var getValueExpression + => _sqlExpressionFactory.Case( + getValueExpression, + [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 + // conversion (based on TimeZone), which we don't want (so avoid translating except on timestamp). + // The translation for ZonedDateTime.Date converts to timestamp before ending up here. + "Date" when instance.TypeMapping is TimestampLocalDateTimeMapping or LegacyTimestampInstantMapping + => _sqlExpressionFactory.Convert(instance, typeof(LocalDate), _typeMappingSource.FindMapping(typeof(LocalDate))!), + + "TimeOfDay" => _sqlExpressionFactory.Convert( + instance, + typeof(LocalTime), + _typeMappingSource.FindMapping(typeof(LocalTime), storeTypeName: "time")), + + _ => null + }; + + /// + /// Constructs the date_part expression. + /// + /// The expression. + /// The name of the date_part to construct. + /// True if the result should be wrapped with floor(...); otherwise, false. + /// + /// The date_part expression. + /// + /// + /// date_part returns doubles, which we floor and cast into ints + /// This also gets rid of sub-second components when retrieving seconds. + /// + private SqlExpression GetDatePartExpression( + SqlExpression instance, + string partName, + bool floor = false) + { + var result = GetDatePartExpressionDouble(instance, partName, floor); + return _sqlExpressionFactory.Convert(result, typeof(int)); + } + + private SqlExpression GetDatePartExpressionDouble( + SqlExpression instance, + string partName, + bool floor = false) + { + var result = _sqlExpressionFactory.Function( + "date_part", + [_sqlExpressionFactory.Constant(partName), instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(double)); + + if (floor) + { + result = _sqlExpressionFactory.Function( + "floor", + [result], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(double)); + } + + return result; + } + + private SqlExpression? TranslateZonedDateTime(SqlExpression instance, MemberInfo member, Type returnType) + { + if (instance is PendingZonedDateTimeExpression pendingZonedDateTime) + { + instance = _sqlExpressionFactory.AtTimeZone( + pendingZonedDateTime.Operand, + pendingZonedDateTime.TimeZoneId, + typeof(LocalDateTime), + _localDateTimeTypeMapping); + + return member == ZonedDateTime_LocalDateTime + ? instance + : TranslateDateTime(instance, member); + } + + // date_part, which is used to extract most components, doesn't have an overload for timestamptz, so passing one directly + // converts it to the local timezone as per TimeZone. Explicitly convert it to a 'timestamp without time zone' in UTC. + // The same works also for the LocalDateTime member. + instance = _sqlExpressionFactory.AtUtc(instance); + + return member == ZonedDateTime_LocalDateTime + ? instance + : TranslateDateTime(instance, member); + } +} diff --git a/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeMethodCallTranslatorPlugin.cs b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeMethodCallTranslatorPlugin.cs new file mode 100644 index 0000000000..72ec2613ea --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Query/Internal/GaussDBNodaTimeMethodCallTranslatorPlugin.cs @@ -0,0 +1,426 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Query.Internal; + +/// +/// Provides translation services for members. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-datetime.html +/// +public class GaussDBNodaTimeMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin +{ + /// + /// 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 GaussDBNodaTimeMethodCallTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + { + Translators = + [ + new GaussDBNodaTimeMethodCallTranslator(typeMappingSource, (GaussDBExpressionFactory)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 virtual IEnumerable Translators { 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 class GaussDBNodaTimeMethodCallTranslator : IMethodCallTranslator +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + + private static readonly MethodInfo SystemClock_GetCurrentInstant = + typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), Type.EmptyTypes)!; + + private static readonly MethodInfo Instant_InUtc = + typeof(Instant).GetRuntimeMethod(nameof(Instant.InUtc), Type.EmptyTypes)!; + + private static readonly MethodInfo Instant_InZone = + 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(GaussDBNodaTimeDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBNodaTimeDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(Instant), typeof(Instant)])!; + + private static readonly MethodInfo ZonedDateTime_ToInstant = + typeof(ZonedDateTime).GetRuntimeMethod(nameof(ZonedDateTime.ToInstant), Type.EmptyTypes)!; + + private static readonly MethodInfo ZonedDateTime_Distance = + typeof(GaussDBNodaTimeDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBNodaTimeDbFunctionsExtensions.Distance), + [typeof(DbFunctions), typeof(ZonedDateTime), typeof(ZonedDateTime)])!; + + private static readonly MethodInfo LocalDateTime_InZoneLeniently = + typeof(LocalDateTime).GetRuntimeMethod(nameof(LocalDateTime.InZoneLeniently), [typeof(DateTimeZone)])!; + + private static readonly MethodInfo LocalDateTime_Distance = + typeof(GaussDBNodaTimeDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBNodaTimeDbFunctionsExtensions.Distance), + [typeof(DbFunctions), typeof(LocalDateTime), typeof(LocalDateTime)])!; + + private static readonly MethodInfo LocalDate_Distance = + typeof(GaussDBNodaTimeDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBNodaTimeDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(LocalDate), typeof(LocalDate)])!; + + 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), [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), [typeof(long)])!; + + private static readonly MethodInfo Period_FromMinutes = + typeof(Period).GetRuntimeMethod(nameof(Period.FromMinutes), [typeof(long)])!; + + private static readonly MethodInfo Period_FromSeconds = + typeof(Period).GetRuntimeMethod(nameof(Period.FromSeconds), [typeof(long)])!; + + private static readonly MethodInfo Interval_Contains + = typeof(Interval).GetRuntimeMethod(nameof(Interval.Contains), [typeof(Instant)])!; + + private static readonly MethodInfo DateInterval_Contains_LocalDate + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), [typeof(LocalDate)])!; + + private static readonly MethodInfo DateInterval_Contains_DateInterval + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), [typeof(DateInterval)])!; + + private static readonly MethodInfo DateInterval_Intersection + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Intersection), [typeof(DateInterval)])!; + + private static readonly MethodInfo DateInterval_Union + = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Union), [typeof(DateInterval)])!; + + private static readonly MethodInfo IDateTimeZoneProvider_get_Item + = typeof(IDateTimeZoneProvider).GetRuntimeMethod("get_Item", [typeof(string)])!; + + private static readonly bool[][] TrueArrays = [[], [true], [true, 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. + /// + public GaussDBNodaTimeMethodCallTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + } + +#pragma warning disable EF1001 + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + var translated = + TranslateInstant(instance, method, arguments) + ?? TranslateZonedDateTime(instance, method, arguments) + ?? TranslateLocalDateTime(instance, method, arguments) + ?? TranslateLocalDate(instance, method, arguments) + ?? TranslatePeriod(instance, method, arguments, logger) + ?? TranslateInterval(instance, method, arguments, logger) + ?? TranslateDateInterval(instance, method, arguments, logger); + + if (translated is not null) + { + return translated; + } + + if (method == IDateTimeZoneProvider_get_Item && instance is PendingDateTimeZoneProviderExpression) + { + // We're translating an expression such as 'DateTimeZoneProviders.Tzdb["Europe/Berlin"]'. + // Note that the .NET type of that expression is DateTimeZone, but we just return the string ID for the time zone. + return arguments[0]; + } + + return null; + } + + private SqlExpression? TranslateInstant( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (method == SystemClock_GetCurrentInstant) + { + return GaussDBNodaTimeTypeMappingSourcePlugin.LegacyTimestampBehavior + ? _sqlExpressionFactory.AtTimeZone( + _sqlExpressionFactory.Function( + "NOW", + [], + nullable: false, + argumentsPropagateNullability: [], + method.ReturnType), + _sqlExpressionFactory.Constant("UTC"), + method.ReturnType) + : _sqlExpressionFactory.Function( + "NOW", + [], + nullable: false, + argumentsPropagateNullability: [], + method.ReturnType, + _typeMappingSource.FindMapping(typeof(Instant), "timestamp with time zone")); + } + + if (method == Instant_InUtc) + { + // Instant -> ZonedDateTime is a no-op (different types in .NET but both mapped to timestamptz in PG) + return instance; + } + + if (method == Instant_InZone) + { + // When InZone is called, we have a mismatch: on the .NET NodaTime side, we have a ZonedDateTime; but on the GaussDB side, + // the AT TIME ZONE expression returns a 'timestamp without time zone' (when applied to a 'timestamp with time zone', which is + // what ZonedDateTime is mapped to). + return new PendingZonedDateTimeExpression(instance!, arguments[0]); + } + + if (method == Instant_ToDateTimeUtc) + { + return _sqlExpressionFactory.Convert( + instance!, + typeof(DateTime), + _typeMappingSource.FindMapping(typeof(DateTime), "timestamp with time zone")); + } + + if (method == Instant_Distance) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.Distance, arguments[1], arguments[2]); + } + + return null; + } + + private SqlExpression? TranslateZonedDateTime( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (method == ZonedDateTime_ToInstant) + { + // We get here with the expression localDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).ToInstant() + if (instance is PendingZonedDateTimeExpression pendingZonedDateTime) + { + return _sqlExpressionFactory.AtTimeZone( + pendingZonedDateTime.Operand, + pendingZonedDateTime.TimeZoneId, + typeof(Instant), + _typeMappingSource.FindMapping(typeof(Instant))); + } + + // Otherwise, ZonedDateTime -> ToInstant is a no-op (different types in .NET but both mapped to timestamptz in PG) + return instance; + } + + if (method == ZonedDateTime_Distance) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.Distance, arguments[1], arguments[2]); + } + + return null; + } + + private SqlExpression? TranslateLocalDateTime( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (method == LocalDateTime_InZoneLeniently) + { + return new PendingZonedDateTimeExpression(instance!, arguments[0]); + } + + if (method == LocalDateTime_Distance) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.Distance, arguments[1], arguments[2]); + } + + return null; + } + + private SqlExpression? TranslateLocalDate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (method == LocalDate_Distance) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.Distance, arguments[1], arguments[2]); + } + + if (method.DeclaringType == typeof(LocalDate)) + { + return method.Name switch + { + nameof(LocalDate.At) => new SqlBinaryExpression( + ExpressionType.Add, + _sqlExpressionFactory.ApplyDefaultTypeMapping(instance!), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), + typeof(LocalDateTime), + _typeMappingSource.FindMapping(typeof(LocalDateTime))), + + nameof(LocalDate.AtMidnight) => new SqlBinaryExpression( + ExpressionType.Add, + _sqlExpressionFactory.ApplyDefaultTypeMapping(instance!), + new SqlConstantExpression(new LocalTime(0, 0, 0), _typeMappingSource.FindMapping(typeof(LocalTime))), + typeof(LocalDateTime), + _typeMappingSource.FindMapping(typeof(LocalDateTime))), + + _ => null + }; + } + return null; + } + + private SqlExpression? TranslatePeriod( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType != typeof(Period)) + { + return null; + } + + if (method == Period_FromYears) + { + return IntervalPart("years", arguments[0]); + } + + if (method == Period_FromMonths) + { + return IntervalPart("months", arguments[0]); + } + + if (method == Period_FromWeeks) + { + return IntervalPart("weeks", arguments[0]); + } + + if (method == Period_FromDays) + { + return IntervalPart("days", arguments[0]); + } + + if (method == Period_FromHours) + { + return IntervalPartOverBigInt("hours", arguments[0]); + } + + if (method == Period_FromMinutes) + { + return IntervalPartOverBigInt("mins", arguments[0]); + } + + if (method == Period_FromSeconds) + { + return IntervalPart( + "secs", _sqlExpressionFactory.Convert(arguments[0], typeof(double), _typeMappingSource.FindMapping(typeof(double)))); + } + + return null; + + static GaussDBFunctionExpression IntervalPart(string datePart, SqlExpression parameter) + => GaussDBFunctionExpression.CreateWithNamedArguments( + "make_interval", + [parameter], + [datePart], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + builtIn: true, + typeof(Period), + typeMapping: null); + + GaussDBFunctionExpression IntervalPartOverBigInt(string datePart, SqlExpression parameter) + { + parameter = _sqlExpressionFactory.ApplyDefaultTypeMapping(parameter); + + // NodaTime Period.FromHours/Minutes/Seconds accept a long parameter, but PG interval_part accepts an int. + // If the parameter happens to be an int cast up to a long, just unwrap it, otherwise downcast from bigint to int + // (this will throw on the PG side if the bigint is out of int range) + if (parameter is SqlUnaryExpression { OperatorType: ExpressionType.Convert } convertExpression + && convertExpression.TypeMapping!.StoreType == "bigint" + && convertExpression.Operand.TypeMapping!.StoreType == "integer") + { + return IntervalPart(datePart, convertExpression.Operand); + } + + return IntervalPart( + datePart, _sqlExpressionFactory.Convert(parameter, typeof(int), _typeMappingSource.FindMapping(typeof(int)))); + } + } + + private SqlExpression? TranslateInterval( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == Interval_Contains) + { + return _sqlExpressionFactory.Contains(instance!, arguments[0]); + } + + return null; + } + + private SqlExpression? TranslateDateInterval( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == DateInterval_Contains_LocalDate + || method == DateInterval_Contains_DateInterval) + { + return _sqlExpressionFactory.Contains(instance!, arguments[0]); + } + + if (method == DateInterval_Intersection) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeIntersect, instance!, arguments[0]); + } + + if (method == DateInterval_Union) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeUnion, instance!, arguments[0]); + } + + return null; + } +#pragma warning restore EF1001 +} diff --git a/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs b/src/EFCore.GaussDB.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs similarity index 88% rename from src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs rename to src/EFCore.GaussDB.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs index 7c2ee5ef94..8d8e37d71a 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs +++ b/src/EFCore.GaussDB.NodaTime/Query/Internal/PendingDateTimeZoneProviderExpression.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Query.Internal; internal class PendingDateTimeZoneProviderExpression : SqlExpression { diff --git a/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs b/src/EFCore.GaussDB.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs similarity index 91% rename from src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs rename to src/EFCore.GaussDB.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs index 41739d7137..ede7a95a27 100644 --- a/src/EFCore.PG.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs +++ b/src/EFCore.GaussDB.NodaTime/Query/Internal/PendingZonedDateTimeExpression.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Query.Internal; internal class PendingZonedDateTimeExpression : SqlExpression { diff --git a/src/EFCore.GaussDB.NodaTime/README.md b/src/EFCore.GaussDB.NodaTime/README.md new file mode 100644 index 0000000000..7ab8c52384 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/README.md @@ -0,0 +1,43 @@ +# GaussDB Entity Framework Core provider for GaussDB + +HuaweiCloud.EntityFrameworkCore.GaussDB is the open source EF Core provider for GaussDB. It allows you to interact with GaussDB via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. + +This package is a plugin which allows you to use the [NodaTime](https://nodatime.org) date/time library when interacting with GaussDB; this provides a better and safer API for dealing with date and time data. + +To use the plugin, simply add `UseNodaTime` as below and use NodaTime types in your entity properties: + +```csharp +await using var ctx = new BlogContext(); +await ctx.Database.EnsureDeletedAsync(); +await ctx.Database.EnsureCreatedAsync(); + +// Insert a Blog +ctx.Blogs.Add(new() +{ + Name = "FooBlog", + CreationTime = SystemClock.Instance.GetCurrentInstant() +}); +await ctx.SaveChangesAsync(); + +// Query all blogs created in 2020 or after +var newBlogs = await ctx.Blogs.Where(b => b.CreationTime >= Instant.FromUtc(2020, 1, 1, 0, 0, 0)).ToListAsync(); + +public class BlogContext : DbContext +{ + public DbSet Blogs { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB( + @"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase", + o => o.UseNodaTime()); +} + +public class Blog +{ + public int Id { get; set; } + public string Name { get; set; } + public Instant CreationTime { get; set; } +} +``` + +The plugin also supports translating most NodaTime methods and properties into corresponding GaussDB date/time operations. For more information, see the [NodaTime plugin documentation page](https://www.npgsql.org/efcore/mapping/nodatime.html). diff --git a/src/EFCore.GaussDB.NodaTime/Scaffolding/Internal/GaussDBNodaTimeCodeGeneratorPlugin.cs b/src/EFCore.GaussDB.NodaTime/Scaffolding/Internal/GaussDBNodaTimeCodeGeneratorPlugin.cs new file mode 100644 index 0000000000..d98fec52ee --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Scaffolding/Internal/GaussDBNodaTimeCodeGeneratorPlugin.cs @@ -0,0 +1,26 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Scaffolding.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 GaussDBNodaTimeCodeGeneratorPlugin : ProviderCodeGeneratorPlugin +{ + private static readonly MethodInfo _useNodaTimeMethodInfo + = typeof(GaussDBNodaTimeDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime), + typeof(GaussDBDbContextOptionsBuilder)); + + /// + /// 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 MethodCallCodeFragment GenerateProviderOptions() + => new(_useNodaTimeMethodInfo); +} diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs similarity index 89% rename from src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs index a8e68760bc..e1dd31d472 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs @@ -1,7 +1,7 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -9,7 +9,7 @@ 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 DateIntervalMultirangeMapping : NpgsqlTypeMapping +public class DateIntervalMultirangeMapping : GaussDBTypeMapping { private readonly DateIntervalRangeMapping _dateIntervalRangeMapping; @@ -20,7 +20,7 @@ public class DateIntervalMultirangeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DateIntervalMultirangeMapping(Type clrType, DateIntervalRangeMapping dateIntervalRangeMapping) - : base("datemultirange", clrType, NpgsqlDbType.DateMultirange) + : base("datemultirange", clrType, GaussDBDbType.DateMultirange) { _dateIntervalRangeMapping = dateIntervalRangeMapping; } @@ -32,7 +32,7 @@ public DateIntervalMultirangeMapping(Type clrType, DateIntervalRangeMapping date /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected DateIntervalMultirangeMapping(RelationalTypeMappingParameters parameters, DateIntervalRangeMapping dateIntervalRangeMapping) - : base(parameters, NpgsqlDbType.DateMultirange) + : base(parameters, GaussDBDbType.DateMultirange) { _dateIntervalRangeMapping = dateIntervalRangeMapping; } @@ -62,5 +62,5 @@ public override RelationalTypeMapping WithStoreTypeAndSize(string storeType, int /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => NpgsqlMultirangeTypeMapping.GenerateNonNullSqlLiteral(value, _dateIntervalRangeMapping, "datemultirange"); + => GaussDBMultirangeTypeMapping.GenerateNonNullSqlLiteral(value, _dateIntervalRangeMapping, "datemultirange"); } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs similarity index 94% rename from src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs index 3a072e8925..92cbe43f30 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs @@ -1,8 +1,8 @@ using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -10,7 +10,7 @@ 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 DateIntervalRangeMapping : NpgsqlTypeMapping +public class DateIntervalRangeMapping : GaussDBTypeMapping { private static readonly ConstructorInfo _constructorWithDates = typeof(DateInterval).GetConstructor([typeof(LocalDate), typeof(LocalDate)])!; @@ -33,7 +33,7 @@ public class DateIntervalRangeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DateIntervalRangeMapping() - : base("daterange", typeof(DateInterval), NpgsqlDbType.DateRange) + : base("daterange", typeof(DateInterval), GaussDBDbType.DateRange) { } @@ -44,7 +44,7 @@ public DateIntervalRangeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected DateIntervalRangeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.DateRange) + : base(parameters, GaussDBDbType.DateRange) { } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateMapping.cs similarity index 92% rename from src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/DateMapping.cs index eea1ef0a5d..ce32b6caea 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateMapping.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Utilties.Util; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ 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 DateMapping : NpgsqlTypeMapping +public class DateMapping : GaussDBTypeMapping { private static readonly ConstructorInfo Constructor = typeof(LocalDate).GetConstructor([typeof(int), typeof(int), typeof(int)])!; @@ -33,7 +33,7 @@ public class DateMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DateMapping() - : base("date", typeof(LocalDate), NpgsqlDbType.Date, JsonLocalDateReaderWriter.Instance) + : base("date", typeof(LocalDate), GaussDBDbType.Date, JsonLocalDateReaderWriter.Instance) { } @@ -44,7 +44,7 @@ public DateMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected DateMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Date) + : base(parameters, GaussDBDbType.Date) { } @@ -86,7 +86,7 @@ protected override string GenerateEmbeddedNonNullSqlLiteral(object value) private static string FormatLocalDate(LocalDate date) { - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { if (date == LocalDate.MinIsoValue) { @@ -124,7 +124,7 @@ public override LocalDate FromJsonTyped(ref Utf8JsonReaderManager manager, objec { var s = manager.CurrentReader.GetString()!; - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { switch (s) { diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateTimeZoneMapping.cs similarity index 98% rename from src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/DateTimeZoneMapping.cs index fea8f021b2..4127182232 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateTimeZoneMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DateTimeZoneMapping.cs @@ -1,6 +1,6 @@ // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// 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/DurationIntervalMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DurationIntervalMapping.cs similarity index 91% rename from src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/DurationIntervalMapping.cs index bb44e93bbc..355ed307e9 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/DurationIntervalMapping.cs @@ -1,9 +1,9 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -11,7 +11,7 @@ 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 DurationIntervalMapping : NpgsqlTypeMapping +public class DurationIntervalMapping : GaussDBTypeMapping { 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)])!; @@ -42,7 +42,7 @@ public class DurationIntervalMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DurationIntervalMapping() - : base("interval", typeof(Duration), NpgsqlDbType.Interval, JsonDurationReaderWriter.Instance) + : base("interval", typeof(Duration), GaussDBDbType.Interval, JsonDurationReaderWriter.Instance) { } @@ -53,7 +53,7 @@ public DurationIntervalMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected DurationIntervalMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Interval) + : base(parameters, GaussDBDbType.Interval) { } @@ -91,7 +91,7 @@ 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) - => NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(((Duration)value).ToTimeSpan()); + => GaussDBIntervalTypeMapping.FormatTimeSpanAsInterval(((Duration)value).ToTimeSpan()); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -142,10 +142,10 @@ private sealed class JsonDurationReaderWriter : JsonValueReaderWriter public static JsonDurationReaderWriter Instance { get; } = new(); public override Duration FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) - => Duration.FromTimeSpan(NpgsqlIntervalTypeMapping.ParseIntervalAsTimeSpan(manager.CurrentReader.GetString()!)); + => Duration.FromTimeSpan(GaussDBIntervalTypeMapping.ParseIntervalAsTimeSpan(manager.CurrentReader.GetString()!)); public override void ToJsonTyped(Utf8JsonWriter writer, Duration value) - => writer.WriteStringValue(NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(value.ToTimeSpan())); + => writer.WriteStringValue(GaussDBIntervalTypeMapping.FormatTimeSpanAsInterval(value.ToTimeSpan())); /// public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); diff --git a/src/EFCore.GaussDB.NodaTime/Storage/Internal/GaussDBNodaTimeTypeMappingSourcePlugin.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/GaussDBNodaTimeTypeMappingSourcePlugin.cs new file mode 100644 index 0000000000..38aa3e5c24 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/GaussDBNodaTimeTypeMappingSourcePlugin.cs @@ -0,0 +1,230 @@ +using System.Collections.Concurrent; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin +{ +#if DEBUG + internal static bool LegacyTimestampBehavior; + internal static bool DisableDateTimeInfinityConversions; +#else + internal static readonly bool LegacyTimestampBehavior; + internal static readonly bool DisableDateTimeInfinityConversions; +#endif + + static GaussDBNodaTimeTypeMappingSourcePlugin() + { + LegacyTimestampBehavior = AppContext.TryGetSwitch("GaussDB.EnableLegacyTimestampBehavior", out var enabled) && enabled; + DisableDateTimeInfinityConversions = AppContext.TryGetSwitch("GaussDB.DisableDateTimeInfinityConversions", out enabled) && enabled; + } + + /// + /// 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 ConcurrentDictionary StoreTypeMappings { 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 ConcurrentDictionary ClrTypeMappings { get; } + + #region TypeMapping + + private readonly TimestampLocalDateTimeMapping _timestampLocalDateTime = TimestampLocalDateTimeMapping.Default; + private readonly LegacyTimestampInstantMapping _legacyTimestampInstant = LegacyTimestampInstantMapping.Default; + + private readonly TimestampTzInstantMapping _timestamptzInstant = TimestampTzInstantMapping.Default; + private readonly TimestampTzZonedDateTimeMapping _timestamptzZonedDateTime = TimestampTzZonedDateTimeMapping.Default; + private readonly TimestampTzOffsetDateTimeMapping _timestamptzOffsetDateTime = TimestampTzOffsetDateTimeMapping.Default; + + private readonly DateMapping _date = DateMapping.Default; + private readonly TimeMapping _time = TimeMapping.Default; + private readonly TimeTzMapping _timetz = TimeTzMapping.Default; + private readonly PeriodIntervalMapping _periodInterval = PeriodIntervalMapping.Default; + private readonly DurationIntervalMapping _durationInterval = DurationIntervalMapping.Default; + + // GaussDB has no native type for representing time zones - it just uses the IANA ID as text. + private readonly DateTimeZoneMapping _timeZone = new("text"); + + // Built-in ranges + private readonly GaussDBRangeTypeMapping _timestampLocalDateTimeRange; + private readonly GaussDBRangeTypeMapping _legacyTimestampInstantRange; + private readonly GaussDBRangeTypeMapping _timestamptzInstantRange; + private readonly GaussDBRangeTypeMapping _timestamptzZonedDateTimeRange; + private readonly GaussDBRangeTypeMapping _timestamptzOffsetDateTimeRange; + private readonly GaussDBRangeTypeMapping _dateRange; + private readonly DateIntervalRangeMapping _dateIntervalRange = new(); + private readonly IntervalRangeMapping _intervalRange = new(); + + #endregion + + /// + /// Constructs an instance of the class. + /// + public GaussDBNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationHelper) + { + _timestampLocalDateTimeRange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tsrange", typeof(GaussDBRange), GaussDBDbType.TimestampRange, _timestampLocalDateTime); + _legacyTimestampInstantRange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tsrange", typeof(GaussDBRange), GaussDBDbType.TimestampRange, _legacyTimestampInstant); + _timestamptzInstantRange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tstzrange", typeof(GaussDBRange), GaussDBDbType.TimestampTzRange, _timestamptzInstant); + _timestamptzZonedDateTimeRange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tstzrange", typeof(GaussDBRange), GaussDBDbType.TimestampTzRange, _timestamptzZonedDateTime); + _timestamptzOffsetDateTimeRange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tstzrange", typeof(GaussDBRange), GaussDBDbType.TimestampTzRange, _timestamptzOffsetDateTime); + _dateRange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "daterange", typeof(GaussDBRange), GaussDBDbType.DateRange, _date); + + var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { + // We currently allow _legacyTimestampInstant even in non-legacy mode, since when upgrading to 6.0 with existing + // 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 + ? [_legacyTimestampInstant, _timestampLocalDateTime] + : [_timestampLocalDateTime, _legacyTimestampInstant] + }, + { + "timestamp with time zone", [_timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime] + }, + { "date", [_date] }, + { "time without time zone", [_time] }, + { "time with time zone", [_timetz] }, + { "interval", [_periodInterval, _durationInterval] }, + { + "tsrange", LegacyTimestampBehavior + ? [_legacyTimestampInstantRange, _timestampLocalDateTimeRange] + : [_timestampLocalDateTimeRange, _legacyTimestampInstantRange] + }, + { + "tstzrange", [ + _intervalRange, _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange + ] + }, + { "daterange", [_dateIntervalRange, _dateRange] } + }; + + // Set up aliases + storeTypeMappings["timestamp"] = storeTypeMappings["timestamp without time zone"]; + storeTypeMappings["timestamptz"] = storeTypeMappings["timestamp with time zone"]; + storeTypeMappings["time"] = storeTypeMappings["time without time zone"]; + storeTypeMappings["timetz"] = storeTypeMappings["time with time zone"]; + + var clrTypeMappings = new Dictionary + { + { typeof(Instant), LegacyTimestampBehavior ? _legacyTimestampInstant : _timestamptzInstant }, + { typeof(LocalDateTime), _timestampLocalDateTime }, + { typeof(ZonedDateTime), _timestamptzZonedDateTime }, + { typeof(OffsetDateTime), _timestamptzOffsetDateTime }, + { typeof(LocalDate), _date }, + { typeof(LocalTime), _time }, + { typeof(OffsetTime), _timetz }, + { typeof(Period), _periodInterval }, + { typeof(Duration), _durationInterval }, + // See DateTimeZone below + + { typeof(GaussDBRange), LegacyTimestampBehavior ? _legacyTimestampInstantRange : _timestamptzInstantRange }, + { typeof(GaussDBRange), _timestampLocalDateTimeRange }, + { typeof(GaussDBRange), _timestamptzZonedDateTimeRange }, + { typeof(GaussDBRange), _timestamptzOffsetDateTimeRange }, + { typeof(GaussDBRange), _dateRange }, + { typeof(DateInterval), _dateIntervalRange }, + { typeof(Interval), _intervalRange }, + }; + + StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); + ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); + } + + /// + /// 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 RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) + => FindBaseMapping(mappingInfo)?.Clone(mappingInfo); + + /// + /// 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? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo) + { + var clrType = mappingInfo.ClrType; + var storeTypeName = mappingInfo.StoreTypeName; + var storeTypeNameBase = mappingInfo.StoreTypeNameBase; + + if (storeTypeName is not null) + { + if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings)) + { + if (clrType is null) + { + return mappings[0]; + } + + foreach (var m in mappings) + { + if (m.ClrType == clrType) + { + return m; + } + } + + return null; + } + + if (StoreTypeMappings.TryGetValue(storeTypeNameBase!, out mappings)) + { + if (clrType is null) + { + return mappings[0]; + } + + foreach (var m in mappings) + { + if (m.ClrType == clrType) + { + return m; + } + } + + return null; + } + } + + if (clrType is not null) + { + if (ClrTypeMappings.TryGetValue(clrType, out var mapping)) + { + return mapping; + } + + if (clrType.IsAssignableTo(typeof(DateTimeZone))) + { + return _timeZone; + } + } + + return null; + } +} diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs similarity index 89% rename from src/EFCore.PG.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs index c99e8f3224..bb183d05e7 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs @@ -1,7 +1,7 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -9,7 +9,7 @@ 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 IntervalMultirangeMapping : NpgsqlTypeMapping +public class IntervalMultirangeMapping : GaussDBTypeMapping { private readonly IntervalRangeMapping _intervalRangeMapping; @@ -20,7 +20,7 @@ public class IntervalMultirangeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public IntervalMultirangeMapping(Type clrType, IntervalRangeMapping intervalRangeMapping) - : base("tstzmultirange", clrType, NpgsqlDbType.TimestampTzMultirange) + : base("tstzmultirange", clrType, GaussDBDbType.TimestampTzMultirange) { _intervalRangeMapping = intervalRangeMapping; } @@ -32,7 +32,7 @@ public IntervalMultirangeMapping(Type clrType, IntervalRangeMapping intervalRang /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected IntervalMultirangeMapping(RelationalTypeMappingParameters parameters, IntervalRangeMapping intervalRangeMapping) - : base(parameters, NpgsqlDbType.DateMultirange) + : base(parameters, GaussDBDbType.DateMultirange) { _intervalRangeMapping = intervalRangeMapping; } @@ -62,5 +62,5 @@ public override RelationalTypeMapping WithStoreTypeAndSize(string storeType, int /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => NpgsqlMultirangeTypeMapping.GenerateNonNullSqlLiteral(value, _intervalRangeMapping, "tstzmultirange"); + => GaussDBMultirangeTypeMapping.GenerateNonNullSqlLiteral(value, _intervalRangeMapping, "tstzmultirange"); } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/IntervalRangeMapping.cs similarity index 95% rename from src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/IntervalRangeMapping.cs index e98372ccb2..67de953f45 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/IntervalRangeMapping.cs @@ -3,10 +3,10 @@ using System.Text; using NodaTime.Text; // ReSharper disable once CheckNamespace -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -14,7 +14,7 @@ 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 IntervalRangeMapping : NpgsqlTypeMapping +public class IntervalRangeMapping : GaussDBTypeMapping { private static readonly ConstructorInfo _constructor = typeof(Interval).GetConstructor([typeof(Instant), typeof(Instant)])!; @@ -37,7 +37,7 @@ public class IntervalRangeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public IntervalRangeMapping() - : base("tstzrange", typeof(Interval), NpgsqlDbType.TimestampTzRange) + : base("tstzrange", typeof(Interval), GaussDBDbType.TimestampTzRange) { } @@ -48,7 +48,7 @@ public IntervalRangeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected IntervalRangeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimestampTzRange) + : base(parameters, GaussDBDbType.TimestampTzRange) { } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs similarity index 93% rename from src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs index c31a179ec5..8017617dc2 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/LegacyTimestampInstantMapping.cs @@ -1,8 +1,8 @@ using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; // Should only be used only with EnableLegacyTimestampBehavior. // However, when upgrading to 6.0 with existing 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. -public class LegacyTimestampInstantMapping : NpgsqlTypeMapping +public class LegacyTimestampInstantMapping : GaussDBTypeMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -30,7 +30,7 @@ public class LegacyTimestampInstantMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public LegacyTimestampInstantMapping() - : base("timestamp without time zone", typeof(Instant), NpgsqlDbType.Timestamp) + : base("timestamp without time zone", typeof(Instant), GaussDBDbType.Timestamp) { } @@ -41,7 +41,7 @@ public LegacyTimestampInstantMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected LegacyTimestampInstantMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Timestamp) + : base(parameters, GaussDBDbType.Timestamp) { } @@ -96,7 +96,7 @@ private string GenerateLiteralCore(object value) { var instant = (Instant)value; - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { if (instant == Instant.MinValue) { diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/PeriodIntervalMapping.cs similarity index 96% rename from src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/PeriodIntervalMapping.cs index 99eba05e12..e41380ff70 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/PeriodIntervalMapping.cs @@ -1,10 +1,10 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -12,7 +12,7 @@ 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 PeriodIntervalMapping : NpgsqlTypeMapping +public class PeriodIntervalMapping : GaussDBTypeMapping { 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)])!; @@ -45,7 +45,7 @@ public class PeriodIntervalMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public PeriodIntervalMapping() - : base("interval", typeof(Period), NpgsqlDbType.Interval, JsonPeriodReaderWriter.Instance) + : base("interval", typeof(Period), GaussDBDbType.Interval, JsonPeriodReaderWriter.Instance) { } @@ -56,7 +56,7 @@ public PeriodIntervalMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected PeriodIntervalMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Interval) + : base(parameters, GaussDBDbType.Interval) { } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimeMapping.cs similarity index 94% rename from src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/TimeMapping.cs index 64c8f4c764..2ce5a1d3f1 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimeMapping.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Utilties.Util; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ 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 TimeMapping : NpgsqlTypeMapping +public class TimeMapping : GaussDBTypeMapping { private static readonly ConstructorInfo ConstructorWithMinutes = typeof(LocalTime).GetConstructor([typeof(int), typeof(int)])!; @@ -41,7 +41,7 @@ public class TimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimeMapping() - : base("time", typeof(LocalTime), NpgsqlDbType.Time, JsonLocalTimeReaderWriter.Instance) + : base("time", typeof(LocalTime), GaussDBDbType.Time, JsonLocalTimeReaderWriter.Instance) { } @@ -52,7 +52,7 @@ public TimeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected TimeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Time) + : base(parameters, GaussDBDbType.Time) { } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimeTzMapping.cs similarity index 95% rename from src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/TimeTzMapping.cs index 7ffeb3116b..390a26beef 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimeTzMapping.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Utilties.Util; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ 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 TimeTzMapping : NpgsqlTypeMapping +public class TimeTzMapping : GaussDBTypeMapping { private static readonly ConstructorInfo OffsetTimeConstructor = typeof(OffsetTime).GetConstructor([typeof(LocalTime), typeof(Offset)])!; @@ -53,7 +53,7 @@ public class TimeTzMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimeTzMapping() - : base("time with time zone", typeof(OffsetTime), NpgsqlDbType.TimeTz, JsonOffsetTimeReaderWriter.Instance) + : base("time with time zone", typeof(OffsetTime), GaussDBDbType.TimeTz, JsonOffsetTimeReaderWriter.Instance) { } @@ -64,7 +64,7 @@ public TimeTzMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected TimeTzMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimeTz) + : base(parameters, GaussDBDbType.TimeTz) { } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs similarity index 93% rename from src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs index a3b2b47f5f..104af0ae7d 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Utilties.Util; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ 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 TimestampLocalDateTimeMapping : NpgsqlTypeMapping +public class TimestampLocalDateTimeMapping : GaussDBTypeMapping { private static readonly ConstructorInfo ConstructorWithMinutes = typeof(LocalDateTime).GetConstructor([typeof(int), typeof(int), typeof(int), typeof(int), typeof(int)])!; @@ -39,7 +39,7 @@ public class TimestampLocalDateTimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampLocalDateTimeMapping() - : base("timestamp without time zone", typeof(LocalDateTime), NpgsqlDbType.Timestamp, JsonLocalDateTimeReaderWriter.Instance) + : base("timestamp without time zone", typeof(LocalDateTime), GaussDBDbType.Timestamp, JsonLocalDateTimeReaderWriter.Instance) { } @@ -50,7 +50,7 @@ public TimestampLocalDateTimeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected TimestampLocalDateTimeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Timestamp) + : base(parameters, GaussDBDbType.Timestamp) { } @@ -103,7 +103,7 @@ protected override string GenerateEmbeddedNonNullSqlLiteral(object value) private static string Format(LocalDateTime localDateTime) { - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { if (localDateTime == LocalDateTime.MinIsoValue) { @@ -153,7 +153,7 @@ public override LocalDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, o { var s = manager.CurrentReader.GetString()!; - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { switch (s) { diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs similarity index 93% rename from src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs index bf6f741b22..bd4785250d 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs @@ -1,10 +1,10 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -12,7 +12,7 @@ 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 TimestampTzInstantMapping : NpgsqlTypeMapping +public class TimestampTzInstantMapping : GaussDBTypeMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,7 +29,7 @@ public class TimestampTzInstantMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampTzInstantMapping() - : base("timestamp with time zone", typeof(Instant), NpgsqlDbType.TimestampTz, JsonInstantReaderWriter.Instance) + : base("timestamp with time zone", typeof(Instant), GaussDBDbType.TimestampTz, JsonInstantReaderWriter.Instance) { } @@ -40,7 +40,7 @@ public TimestampTzInstantMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected TimestampTzInstantMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimestampTz) + : base(parameters, GaussDBDbType.TimestampTz) { } @@ -93,7 +93,7 @@ protected override string GenerateEmbeddedNonNullSqlLiteral(object value) private static string Format(Instant instant) { - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { if (instant == Instant.MinValue) { @@ -134,7 +134,7 @@ public override Instant FromJsonTyped(ref Utf8JsonReaderManager manager, object? { var s = manager.CurrentReader.GetString()!; - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + if (!GaussDBNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { switch (s) { diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs similarity index 95% rename from src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs index 8d54d819bf..c6cbd31425 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Utilties.Util; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ 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 TimestampTzOffsetDateTimeMapping : NpgsqlTypeMapping +public class TimestampTzOffsetDateTimeMapping : GaussDBTypeMapping { private static readonly ConstructorInfo Constructor = typeof(OffsetDateTime).GetConstructor([typeof(LocalDateTime), typeof(Offset)])!; @@ -39,7 +39,7 @@ public class TimestampTzOffsetDateTimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampTzOffsetDateTimeMapping() - : base("timestamp with time zone", typeof(OffsetDateTime), NpgsqlDbType.TimestampTz, JsonOffsetDateTimeReaderWriter.Instance) + : base("timestamp with time zone", typeof(OffsetDateTime), GaussDBDbType.TimestampTz, JsonOffsetDateTimeReaderWriter.Instance) { } @@ -50,7 +50,7 @@ public TimestampTzOffsetDateTimeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected TimestampTzOffsetDateTimeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimestampTz) + : base(parameters, GaussDBDbType.TimestampTz) { } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs similarity index 96% rename from src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs rename to src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs index 8ed3c499f7..324fb8a43f 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs +++ b/src/EFCore.GaussDB.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs @@ -2,10 +2,10 @@ using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using NodaTime.TimeZones; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -13,7 +13,7 @@ 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 TimestampTzZonedDateTimeMapping : NpgsqlTypeMapping +public class TimestampTzZonedDateTimeMapping : GaussDBTypeMapping { private static readonly ZonedDateTimePattern Pattern = ZonedDateTimePattern.CreateWithInvariantCulture( @@ -35,7 +35,7 @@ public class TimestampTzZonedDateTimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampTzZonedDateTimeMapping() - : base("timestamp with time zone", typeof(ZonedDateTime), NpgsqlDbType.TimestampTz, JsonZonedDateTimeReaderWriter.Instance) + : base("timestamp with time zone", typeof(ZonedDateTime), GaussDBDbType.TimestampTz, JsonZonedDateTimeReaderWriter.Instance) { } @@ -46,7 +46,7 @@ public TimestampTzZonedDateTimeMapping() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected TimestampTzZonedDateTimeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimestampTz) + : base(parameters, GaussDBDbType.TimestampTz) { } diff --git a/src/EFCore.GaussDB.NodaTime/Utilties/Util.cs b/src/EFCore.GaussDB.NodaTime/Utilties/Util.cs new file mode 100644 index 0000000000..8cdc7695d5 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/Utilties/Util.cs @@ -0,0 +1,10 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Utilties; + +internal static class Util +{ + internal static NewExpression ConstantNew(ConstructorInfo constructor, params object[] parameters) + => Expression.New(constructor, parameters.Select(p => Expression.Constant(p)).ToArray()); + + internal static MethodCallExpression ConstantCall(MethodInfo method, params object[] parameters) + => Expression.Call(method, parameters.Select(p => Expression.Constant(p)).ToArray()); +} diff --git a/src/EFCore.GaussDB.NodaTime/build/netstandard2.0/HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.targets b/src/EFCore.GaussDB.NodaTime/build/netstandard2.0/HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.targets new file mode 100644 index 0000000000..1f525c0c74 --- /dev/null +++ b/src/EFCore.GaussDB.NodaTime/build/netstandard2.0/HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.targets @@ -0,0 +1,46 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + $(IntermediateOutputPath)EFCoreGaussDBNodaTime$(DefaultLanguageSourceExtension) + + + + + + + CompileBefore + + + + + CompileAfter + + + + + + + Compile + + + + + + + <_Parameter1>HuaweiCloud.EntityFrameworkCore.GaussDB.Design.Internal.GaussDBNodaTimeDesignTimeServices, HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime + <_Parameter2>HuaweiCloud.EntityFrameworkCore.GaussDB + + + + + + + + diff --git a/src/EFCore.GaussDB/Design/Internal/GaussDBAnnotationCodeGenerator.cs b/src/EFCore.GaussDB/Design/Internal/GaussDBAnnotationCodeGenerator.cs new file mode 100644 index 0000000000..a5f83ed6ed --- /dev/null +++ b/src/EFCore.GaussDB/Design/Internal/GaussDBAnnotationCodeGenerator.cs @@ -0,0 +1,447 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Design.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 GaussDBAnnotationCodeGenerator : AnnotationCodeGenerator +{ + #region MethodInfos + + private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo1 + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string)); + + private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo2 + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string), typeof(string), + typeof(string)); + + private static readonly MethodInfo ModelHasPostgresEnumMethodInfo1 + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string[])); + + private static readonly MethodInfo ModelHasPostgresEnumMethodInfo2 + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string[])); + + private static readonly MethodInfo ModelHasPostgresRangeMethodInfo1 + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string)); + + private static readonly MethodInfo ModelHasPostgresRangeMethodInfo2 + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(string), typeof(string)); + + private static readonly MethodInfo ModelUseSerialColumnsMethodInfo + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.UseSerialColumns), typeof(ModelBuilder)); + + private static readonly MethodInfo ModelUseIdentityAlwaysColumnsMethodInfo + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.UseIdentityAlwaysColumns), typeof(ModelBuilder)); + + private static readonly MethodInfo ModelUseIdentityByDefaultColumnsMethodInfo + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.UseIdentityByDefaultColumns), typeof(ModelBuilder)); + + private static readonly MethodInfo ModelUseHiLoMethodInfo + = typeof(GaussDBModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.UseHiLo), typeof(ModelBuilder), typeof(string), typeof(string)); + + private static readonly MethodInfo ModelHasAnnotationMethodInfo + = typeof(ModelBuilder).GetRequiredRuntimeMethod( + nameof(ModelBuilder.HasAnnotation), typeof(string), typeof(object)); + + private static readonly MethodInfo ModelUseKeySequencesMethodInfo + = typeof(GaussDBModelBuilderExtensions).GetRuntimeMethod( + nameof(GaussDBModelBuilderExtensions.UseKeySequences), [typeof(ModelBuilder), typeof(string), typeof(string)])!; + + private static readonly MethodInfo EntityTypeIsUnloggedMethodInfo + = typeof(GaussDBEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBEntityTypeBuilderExtensions.IsUnlogged), typeof(EntityTypeBuilder), typeof(bool)); + + private static readonly MethodInfo PropertyUseSerialColumnMethodInfo + = typeof(GaussDBPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBPropertyBuilderExtensions.UseSerialColumn), typeof(PropertyBuilder)); + + private static readonly MethodInfo PropertyUseIdentityAlwaysColumnMethodInfo + = typeof(GaussDBPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBPropertyBuilderExtensions.UseIdentityAlwaysColumn), typeof(PropertyBuilder)); + + private static readonly MethodInfo PropertyUseIdentityByDefaultColumnMethodInfo + = typeof(GaussDBPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBPropertyBuilderExtensions.UseIdentityByDefaultColumn), typeof(PropertyBuilder)); + + private static readonly MethodInfo PropertyUseHiLoMethodInfo + = typeof(GaussDBPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBPropertyBuilderExtensions.UseHiLo), typeof(PropertyBuilder), typeof(string), typeof(string)); + + private static readonly MethodInfo PropertyHasIdentityOptionsMethodInfo + = typeof(GaussDBPropertyBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBPropertyBuilderExtensions.HasIdentityOptions), typeof(PropertyBuilder), typeof(long?), typeof(long?), + typeof(long?), typeof(long?), typeof(bool?), typeof(long?)); + + private static readonly MethodInfo PropertyUseSequenceMethodInfo + = typeof(GaussDBPropertyBuilderExtensions).GetRuntimeMethod( + nameof(GaussDBPropertyBuilderExtensions.UseSequence), [typeof(PropertyBuilder), typeof(string), typeof(string)])!; + + private static readonly MethodInfo IndexUseCollationMethodInfo + = typeof(GaussDBIndexBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBIndexBuilderExtensions.UseCollation), typeof(IndexBuilder), typeof(string[])); + + private static readonly MethodInfo IndexHasMethodMethodInfo + = typeof(GaussDBIndexBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBIndexBuilderExtensions.HasMethod), typeof(IndexBuilder), typeof(string)); + + private static readonly MethodInfo IndexHasOperatorsMethodInfo + = typeof(GaussDBIndexBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBIndexBuilderExtensions.HasOperators), typeof(IndexBuilder), typeof(string[])); + + private static readonly MethodInfo IndexHasNullSortOrderMethodInfo + = typeof(GaussDBIndexBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBIndexBuilderExtensions.HasNullSortOrder), typeof(IndexBuilder), typeof(NullSortOrder[])); + + private static readonly MethodInfo IndexIncludePropertiesMethodInfo + = typeof(GaussDBIndexBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBIndexBuilderExtensions.IncludeProperties), typeof(IndexBuilder), typeof(string[])); + + private static readonly MethodInfo IndexAreNullsDistinctMethodInfo + = typeof(GaussDBIndexBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBIndexBuilderExtensions.AreNullsDistinct), typeof(IndexBuilder), typeof(bool)); + + #endregion MethodInfos + + /// + /// 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 GaussDBAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies) + : base(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 + /// 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 IsHandledByConvention(IModel model, IAnnotation annotation) + { + Check.NotNull(model, nameof(model)); + Check.NotNull(annotation, nameof(annotation)); + + if (annotation.Name == RelationalAnnotationNames.DefaultSchema + && (string?)annotation.Value == "public") + { + return true; + } + + return false; + } + + /// + /// 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 IsHandledByConvention(IIndex index, IAnnotation annotation) + { + Check.NotNull(index, nameof(index)); + Check.NotNull(annotation, nameof(annotation)); + + if (annotation.Name == GaussDBAnnotationNames.IndexMethod + && (string?)annotation.Value == "btree") + { + return true; + } + + return false; + } + + /// + /// 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 IsHandledByConvention(IProperty property, IAnnotation annotation) + { + Check.NotNull(property, nameof(property)); + Check.NotNull(annotation, nameof(annotation)); + + // The default by-convention value generation strategy is serial in pre-10 GaussDB, + // and IdentityByDefault otherwise. + if (annotation.Name == GaussDBAnnotationNames.ValueGenerationStrategy) + { + // Note: both serial and identity-by-default columns are considered by-convention - we don't want + // to assume that the GaussDB version of the scaffolded database necessarily determines the + // version of the database that the scaffolded model will target. This makes life difficult for + // models with mixed strategies but that's an edge case. + return (GaussDBValueGenerationStrategy?)annotation.Value switch + { + GaussDBValueGenerationStrategy.SerialColumn => true, + GaussDBValueGenerationStrategy.IdentityByDefaultColumn => true, + _ => false + }; + } + + return false; + } + + /// + /// 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 IReadOnlyList GenerateFluentApiCalls( + IModel model, + IDictionary annotations) + { + var fragments = new List(base.GenerateFluentApiCalls(model, annotations)); + + if (GenerateValueGenerationStrategy(annotations, onModel: true) is { } valueGenerationStrategy) + { + fragments.Add(valueGenerationStrategy); + } + + return fragments; + } + + /// + /// 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 MethodCallCodeFragment? GenerateFluentApi(IModel model, IAnnotation annotation) + { + Check.NotNull(model, nameof(model)); + Check.NotNull(annotation, nameof(annotation)); + + if (annotation.Name.StartsWith(GaussDBAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)) + { + var extension = new GaussDBExtension(model, annotation.Name); + + return extension.Schema is "public" or null + ? new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo1, extension.Name) + : new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo2, extension.Schema, extension.Name); + } + + if (annotation.Name.StartsWith(GaussDBAnnotationNames.EnumPrefix, StringComparison.Ordinal)) + { + var enumTypeDef = new GaussDBEnum(model, annotation.Name); + + return enumTypeDef.Schema is null + ? new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo1, enumTypeDef.Name, enumTypeDef.Labels) + : new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo2, enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); + } + + if (annotation.Name.StartsWith(GaussDBAnnotationNames.RangePrefix, StringComparison.Ordinal)) + { + var rangeTypeDef = new Metadata.GaussDBRange(model, annotation.Name); + + if (rangeTypeDef.Schema is null + && rangeTypeDef.CanonicalFunction is null + && rangeTypeDef.SubtypeOpClass is null + && rangeTypeDef.Collation is null + && rangeTypeDef.SubtypeDiff is null) + { + return new MethodCallCodeFragment(ModelHasPostgresRangeMethodInfo1, rangeTypeDef.Name, rangeTypeDef.Subtype); + } + + return new MethodCallCodeFragment( + ModelHasPostgresRangeMethodInfo2, + rangeTypeDef.Schema, + rangeTypeDef.Name, + rangeTypeDef.Subtype, + rangeTypeDef.CanonicalFunction, + rangeTypeDef.SubtypeOpClass, + rangeTypeDef.Collation, + rangeTypeDef.SubtypeDiff); + } + + return 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 override MethodCallCodeFragment? GenerateFluentApi(IEntityType entityType, IAnnotation annotation) + { + Check.NotNull(entityType, nameof(entityType)); + Check.NotNull(annotation, nameof(annotation)); + + if (annotation.Name == GaussDBAnnotationNames.UnloggedTable) + { + return new MethodCallCodeFragment(EntityTypeIsUnloggedMethodInfo, annotation.Value); + } + + return 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 override IReadOnlyList GenerateFluentApiCalls( + IProperty property, + IDictionary annotations) + { + var fragments = new List(base.GenerateFluentApiCalls(property, annotations)); + + if (GenerateValueGenerationStrategy(annotations, onModel: false) is { } valueGenerationStrategy) + { + fragments.Add(valueGenerationStrategy); + } + + if (GenerateIdentityOptions(annotations) is { } identityOptionsFragment) + { + fragments.Add(identityOptionsFragment); + } + + return fragments; + } + + private MethodCallCodeFragment? GenerateValueGenerationStrategy(IDictionary annotations, bool onModel) + { + if (!TryGetAndRemove(annotations, GaussDBAnnotationNames.ValueGenerationStrategy, out GaussDBValueGenerationStrategy strategy)) + { + return null; + } + + switch (strategy) + { + case GaussDBValueGenerationStrategy.SerialColumn: + return new MethodCallCodeFragment(onModel ? ModelUseSerialColumnsMethodInfo : PropertyUseSerialColumnMethodInfo); + + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + return new MethodCallCodeFragment( + onModel ? ModelUseIdentityAlwaysColumnsMethodInfo : PropertyUseIdentityAlwaysColumnMethodInfo); + + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + return new MethodCallCodeFragment( + onModel ? ModelUseIdentityByDefaultColumnsMethodInfo : PropertyUseIdentityByDefaultColumnMethodInfo); + + case GaussDBValueGenerationStrategy.SequenceHiLo: + { + var name = GetAndRemove(GaussDBAnnotationNames.HiLoSequenceName)!; + var schema = GetAndRemove(GaussDBAnnotationNames.HiLoSequenceSchema); + return new MethodCallCodeFragment( + onModel ? ModelUseHiLoMethodInfo : PropertyUseHiLoMethodInfo, + (name, schema) switch + { + (null, null) => [], + (_, null) => [name], + _ => [name!, schema] + }); + } + + case GaussDBValueGenerationStrategy.Sequence: + { + var nameOrSuffix = GetAndRemove( + onModel ? GaussDBAnnotationNames.SequenceNameSuffix : GaussDBAnnotationNames.SequenceName); + + var schema = GetAndRemove(GaussDBAnnotationNames.SequenceSchema); + return new MethodCallCodeFragment( + onModel ? ModelUseKeySequencesMethodInfo : PropertyUseSequenceMethodInfo, + (name: nameOrSuffix, schema) switch + { + (null, null) => [], + (_, null) => [nameOrSuffix], + _ => [nameOrSuffix!, schema] + }); + } + case GaussDBValueGenerationStrategy.None: + return new MethodCallCodeFragment( + ModelHasAnnotationMethodInfo, GaussDBAnnotationNames.ValueGenerationStrategy, GaussDBValueGenerationStrategy.None); + + default: + throw new ArgumentOutOfRangeException(strategy.ToString()); + } + + T? GetAndRemove(string annotationName) + => TryGetAndRemove(annotations, annotationName, out T? annotationValue) + ? annotationValue + : default; + } + + private MethodCallCodeFragment? GenerateIdentityOptions(IDictionary annotations) + { + if (!TryGetAndRemove( + annotations, GaussDBAnnotationNames.IdentityOptions, + out string? annotationValue)) + { + return null; + } + + var identityOptions = IdentitySequenceOptionsData.Deserialize(annotationValue); + return new MethodCallCodeFragment( + PropertyHasIdentityOptionsMethodInfo, + identityOptions.StartValue, + identityOptions.IncrementBy == 1 ? null : (long?)identityOptions.IncrementBy, + identityOptions.MinValue, + identityOptions.MaxValue, + identityOptions.IsCyclic ? true : null, + identityOptions.NumbersToCache == 1 ? null : (long?)identityOptions.NumbersToCache); + } + + /// + /// 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 MethodCallCodeFragment? GenerateFluentApi(IIndex index, IAnnotation annotation) + => annotation.Name switch + { + RelationalAnnotationNames.Collation + => new MethodCallCodeFragment(IndexUseCollationMethodInfo, annotation.Value), + + GaussDBAnnotationNames.IndexMethod + => new MethodCallCodeFragment(IndexHasMethodMethodInfo, annotation.Value), + GaussDBAnnotationNames.IndexOperators + => new MethodCallCodeFragment(IndexHasOperatorsMethodInfo, annotation.Value), + GaussDBAnnotationNames.IndexNullSortOrder + => new MethodCallCodeFragment(IndexHasNullSortOrderMethodInfo, annotation.Value), + GaussDBAnnotationNames.IndexInclude + => new MethodCallCodeFragment(IndexIncludePropertiesMethodInfo, annotation.Value), + GaussDBAnnotationNames.NullsDistinct + => new MethodCallCodeFragment(IndexAreNullsDistinctMethodInfo, annotation.Value), + _ => null + }; + + private static bool TryGetAndRemove( + IDictionary annotations, + string annotationName, + [NotNullWhen(true)] out T? annotationValue) + { + if (annotations.TryGetValue(annotationName, out var annotation) + && annotation.Value is not null) + { + annotations.Remove(annotationName); + annotationValue = (T)annotation.Value; + return true; + } + + annotationValue = default; + return false; + } +} diff --git a/src/EFCore.GaussDB/Design/Internal/GaussDBCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.GaussDB/Design/Internal/GaussDBCSharpRuntimeAnnotationCodeGenerator.cs new file mode 100644 index 0000000000..d19ab947e2 --- /dev/null +++ b/src/EFCore.GaussDB/Design/Internal/GaussDBCSharpRuntimeAnnotationCodeGenerator.cs @@ -0,0 +1,378 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Design.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Design.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. +/// +#pragma warning disable EF1001 // Internal EF Core API usage. +public class GaussDBCSharpRuntimeAnnotationCodeGenerator + : 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 + /// 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 GaussDBCSharpRuntimeAnnotationCodeGenerator( + CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies, + RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + /// 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 bool Create( + CoreTypeMapping typeMapping, + CSharpRuntimeAnnotationCodeGeneratorParameters parameters, + ValueComparer? valueComparer = null, + ValueComparer? keyValueComparer = null, + ValueComparer? providerValueComparer = null) + { + _typeMappingNestingCount++; + + try + { + var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer); + AddGaussDBTypeMappingTweaks(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 AddGaussDBTypeMappingTweaks( + CoreTypeMapping typeMapping, + CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + var mainBuilder = parameters.MainBuilder; + + var npgsqlDbTypeBasedDefaultInstance = typeMapping switch + { + GaussDBStringTypeMapping => GaussDBStringTypeMapping.Default, + GaussDBUIntTypeMapping => GaussDBUIntTypeMapping.Default, + GaussDBULongTypeMapping => GaussDBULongTypeMapping.Default, + // GaussDBMultirangeTypeMapping => GaussDBMultirangeTypeMapping.Default, + _ => (IGaussDBTypeMapping?)null + }; + + if (npgsqlDbTypeBasedDefaultInstance is not null) + { + CheckElementTypeMapping(); + + var npgsqlDbType = ((IGaussDBTypeMapping)typeMapping).GaussDBDbType; + + if (npgsqlDbType != npgsqlDbTypeBasedDefaultInstance.GaussDBDbType) + { + mainBuilder.AppendLine(";"); + + mainBuilder.Append( + $"{parameters.TargetName}.TypeMapping = (({typeMapping.GetType().Name}){parameters.TargetName}.TypeMapping).Clone(npgsqlDbType: "); + + mainBuilder + .Append(nameof(GaussDBTypes)) + .Append(".") + .Append(nameof(GaussDBDbType)) + .Append(".") + .Append(npgsqlDbType.ToString()); + + mainBuilder + .Append(")") + .DecrementIndent(); + } + + } + + switch (typeMapping) + { + case GaussDBEnumTypeMapping enumTypeMapping: + CheckElementTypeMapping(); + + var code = Dependencies.CSharpHelper; + mainBuilder.AppendLine(";"); + + mainBuilder.AppendLine( + $"{parameters.TargetName}.TypeMapping = ((GaussDBEnumTypeMapping){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) + { + mainBuilder + .Append('[') + .Append(code.UnknownLiteral(enumValue)) + .Append(']') + .Append(" = ") + .Append(code.Literal(label)) + .AppendLine(","); + } + + mainBuilder + .Append("}") + .DecrementIndent() + .Append(")") + .DecrementIndent(); + + break; + + case GaussDBRangeTypeMapping rangeTypeMapping: + { + CheckElementTypeMapping(); + + var defaultInstance = GaussDBRangeTypeMapping.Default; + + var npgsqlDbTypeDifferent = rangeTypeMapping.GaussDBDbType != defaultInstance.GaussDBDbType; + var subtypeTypeMappingIsDifferent = rangeTypeMapping.SubtypeMapping != defaultInstance.SubtypeMapping; + + if (npgsqlDbTypeDifferent || subtypeTypeMappingIsDifferent) + { + mainBuilder.AppendLine(";"); + + mainBuilder.AppendLine( + $"{parameters.TargetName}.TypeMapping = ((GaussDBRangeTypeMapping){parameters.TargetName}.TypeMapping).Clone(") + .IncrementIndent(); + + mainBuilder + .Append("npgsqlDbType: ") + .Append(nameof(GaussDBTypes)) + .Append(".") + .Append(nameof(GaussDBDbType)) + .Append(".") + .Append(rangeTypeMapping.GaussDBDbType.ToString()) + .AppendLine(","); + + mainBuilder.Append("subtypeTypeMapping: "); + + Create(rangeTypeMapping.SubtypeMapping, parameters); + + mainBuilder + .Append(")") + .DecrementIndent(); + } + + break; + } + } + + void CheckElementTypeMapping() + { + if (_typeMappingNestingCount > 1) + { + throw new NotSupportedException( + $"Non-default GaussDB 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."); + } + } + } + + /// + public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + annotations.Remove(GaussDBAnnotationNames.DatabaseTemplate); + annotations.Remove(GaussDBAnnotationNames.Tablespace); + annotations.Remove(GaussDBAnnotationNames.CollationDefinitionPrefix); + +#pragma warning disable CS0618 + annotations.Remove(GaussDBAnnotationNames.DefaultColumnCollation); +#pragma warning restore CS0618 + + foreach (var annotationName in annotations.Keys.Where( + k => + k.StartsWith(GaussDBAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) + || k.StartsWith(GaussDBAnnotationNames.EnumPrefix, StringComparison.Ordinal) + || k.StartsWith(GaussDBAnnotationNames.RangePrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + + base.Generate(model, parameters); + } + + /// + public override void Generate(IRelationalModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + annotations.Remove(GaussDBAnnotationNames.DatabaseTemplate); + annotations.Remove(GaussDBAnnotationNames.Tablespace); + annotations.Remove(GaussDBAnnotationNames.CollationDefinitionPrefix); + +#pragma warning disable CS0618 + annotations.Remove(GaussDBAnnotationNames.DefaultColumnCollation); +#pragma warning restore CS0618 + + foreach (var annotationName in annotations.Keys.Where( + k => + k.StartsWith(GaussDBAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) + || k.StartsWith(GaussDBAnnotationNames.EnumPrefix, StringComparison.Ordinal) + || k.StartsWith(GaussDBAnnotationNames.RangePrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + + base.Generate(model, parameters); + } + + /// + public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + annotations.Remove(GaussDBAnnotationNames.IdentityOptions); + annotations.Remove(GaussDBAnnotationNames.TsVectorConfig); + annotations.Remove(GaussDBAnnotationNames.TsVectorProperties); + annotations.Remove(GaussDBAnnotationNames.CompressionMethod); + + if (!annotations.ContainsKey(GaussDBAnnotationNames.ValueGenerationStrategy)) + { + annotations[GaussDBAnnotationNames.ValueGenerationStrategy] = property.GetValueGenerationStrategy(); + } + } + + base.Generate(property, parameters); + } + + /// + public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(GaussDBAnnotationNames.IdentityOptions); + annotations.Remove(GaussDBAnnotationNames.TsVectorConfig); + annotations.Remove(GaussDBAnnotationNames.TsVectorProperties); + annotations.Remove(GaussDBAnnotationNames.CompressionMethod); + } + + base.Generate(column, parameters); + } + + /// + public override void Generate(IIndex index, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(GaussDBAnnotationNames.IndexMethod); + annotations.Remove(GaussDBAnnotationNames.IndexOperators); + annotations.Remove(GaussDBAnnotationNames.IndexSortOrder); + annotations.Remove(GaussDBAnnotationNames.IndexNullSortOrder); + annotations.Remove(GaussDBAnnotationNames.IndexInclude); + annotations.Remove(GaussDBAnnotationNames.CreatedConcurrently); + annotations.Remove(GaussDBAnnotationNames.NullsDistinct); + + foreach (var annotationName in annotations.Keys.Where( + k => k.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + + base.Generate(index, parameters); + } + + /// + public override void Generate(ITableIndex index, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(GaussDBAnnotationNames.IndexMethod); + annotations.Remove(GaussDBAnnotationNames.IndexOperators); + annotations.Remove(GaussDBAnnotationNames.IndexSortOrder); + annotations.Remove(GaussDBAnnotationNames.IndexNullSortOrder); + annotations.Remove(GaussDBAnnotationNames.IndexInclude); + annotations.Remove(GaussDBAnnotationNames.CreatedConcurrently); + annotations.Remove(GaussDBAnnotationNames.NullsDistinct); + + foreach (var annotationName in annotations.Keys.Where( + k => k.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + + base.Generate(index, parameters); + } + + /// + public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(GaussDBAnnotationNames.UnloggedTable); + annotations.Remove(CockroachDbAnnotationNames.InterleaveInParent); + + foreach (var annotationName in annotations.Keys.Where( + k => k.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + + base.Generate(entityType, parameters); + } + + /// + public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + + annotations.Remove(GaussDBAnnotationNames.UnloggedTable); + annotations.Remove(CockroachDbAnnotationNames.InterleaveInParent); + + foreach (var annotationName in annotations.Keys.Where( + k => k.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + + base.Generate(table, parameters); + } +} diff --git a/src/EFCore.GaussDB/Design/Internal/GaussDBDesignTimeServices.cs b/src/EFCore.GaussDB/Design/Internal/GaussDBDesignTimeServices.cs new file mode 100644 index 0000000000..bc23849e66 --- /dev/null +++ b/src/EFCore.GaussDB/Design/Internal/GaussDBDesignTimeServices.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Design.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Design.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 GaussDBDesignTimeServices : IDesignTimeServices +{ + /// + /// 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 void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + serviceCollection.AddEntityFrameworkGaussDB(); +#pragma warning disable EF1001 // Internal EF Core API usage. + new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) + .TryAdd() +#pragma warning restore EF1001 // Internal EF Core API usage. + .TryAdd() + .TryAdd() + .TryAdd() + .TryAddCoreServices(); + } +} diff --git a/src/EFCore.GaussDB/Diagnostics/GaussDBEfEventId.cs b/src/EFCore.GaussDB/Diagnostics/GaussDBEfEventId.cs new file mode 100644 index 0000000000..74ce9736fe --- /dev/null +++ b/src/EFCore.GaussDB/Diagnostics/GaussDBEfEventId.cs @@ -0,0 +1,227 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore.Diagnostics; + +/// +/// +/// Event IDs for GaussDB events that correspond to messages logged to an +/// and events sent to a . +/// +/// +/// These IDs are also used with to configure the +/// behavior of warnings. +/// +/// +public static class GaussDBEfEventId +{ + // Warning: These values must not change between releases. + // Only add new values to the end of sections, never in the middle. + // Try to use {Noun}{Verb} naming and be consistent with existing names. + private enum Id + { + // Model validation events + // Scaffolding events + ColumnFound = CoreEventId.ProviderDesignBaseId, + + //ColumnNotNamedWarning, + //ColumnSkipped, + //ForeignKeyColumnMissingWarning, + //ForeignKeyColumnNotNamedWarning, + //ForeignKeyColumnsNotMappedWarning, + //ForeignKeyNotNamedWarning, + ForeignKeyReferencesMissingPrincipalTableWarning, + + //IndexColumnFound, + //IndexColumnNotNamedWarning, + //IndexColumnSkipped, + //IndexColumnsNotMappedWarning, + //IndexNotNamedWarning, + //IndexTableMissingWarning, + MissingSchemaWarning, + MissingTableWarning, + SequenceFound, + + //SequenceNotNamedWarning, + TableFound, + + //TableSkipped, + //ForeignKeyTableMissingWarning, + PrimaryKeyFound, + UniqueConstraintFound, + IndexFound, + ForeignKeyFound, + ForeignKeyPrincipalColumnMissingWarning, + EnumColumnSkippedWarning, + ExpressionIndexSkippedWarning, + UnsupportedColumnIndexSkippedWarning, + UnsupportedConstraintIndexSkippedWarning, + CollationFound + } + + private static readonly string ScaffoldingPrefix = DbLoggerCategory.Scaffolding.Name + "."; + + private static EventId MakeScaffoldingId(Id id) + => new((int)id, ScaffoldingPrefix + id); + + /// + /// + /// A column was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId ColumnFound = MakeScaffoldingId(Id.ColumnFound); + + /// + /// + /// The database is missing a schema. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId MissingSchemaWarning = MakeScaffoldingId(Id.MissingSchemaWarning); + + /// + /// + /// A collation was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId CollationFound = MakeScaffoldingId(Id.CollationFound); + + /// + /// + /// The database is missing a table. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId MissingTableWarning = MakeScaffoldingId(Id.MissingTableWarning); + + /// + /// + /// A foreign key references a missing table at the principal end. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId ForeignKeyReferencesMissingPrincipalTableWarning = + MakeScaffoldingId(Id.ForeignKeyReferencesMissingPrincipalTableWarning); + + /// + /// + /// A table was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId TableFound = MakeScaffoldingId(Id.TableFound); + + /// + /// + /// A sequence was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId SequenceFound = MakeScaffoldingId(Id.SequenceFound); + + /// + /// + /// A primary key was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId PrimaryKeyFound = MakeScaffoldingId(Id.PrimaryKeyFound); + + /// + /// + /// A unique constraint was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId UniqueConstraintFound = MakeScaffoldingId(Id.UniqueConstraintFound); + + /// + /// + /// An index was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId IndexFound = MakeScaffoldingId(Id.IndexFound); + + /// + /// + /// A foreign key was found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId ForeignKeyFound = MakeScaffoldingId(Id.ForeignKeyFound); + + /// + /// + /// A principal column referenced by a foreign key was not found. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId ForeignKeyPrincipalColumnMissingWarning = MakeScaffoldingId(Id.ForeignKeyPrincipalColumnMissingWarning); + + /// + /// + /// Enum column cannot be scaffolded, define a CLR enum type and add the property manually. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId EnumColumnSkippedWarning = MakeScaffoldingId(Id.EnumColumnSkippedWarning); + + /// + /// + /// Expression index cannot be scaffolded, expression indices aren't supported and must be added via raw SQL in migrations. + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId ExpressionIndexSkippedWarning = MakeScaffoldingId(Id.ExpressionIndexSkippedWarning); + + /// + /// + /// Index '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId UnsupportedColumnIndexSkippedWarning = MakeScaffoldingId(Id.UnsupportedColumnIndexSkippedWarning); + + /// + /// + /// Constraint '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). + /// + /// + /// This event is in the category. + /// + /// + public static readonly EventId UnsupportedColumnConstraintSkippedWarning = + MakeScaffoldingId(Id.UnsupportedConstraintIndexSkippedWarning); +} diff --git a/src/EFCore.GaussDB/Diagnostics/Internal/GaussDBLoggingDefinitions.cs b/src/EFCore.GaussDB/Diagnostics/Internal/GaussDBLoggingDefinitions.cs new file mode 100644 index 0000000000..efff4f9746 --- /dev/null +++ b/src/EFCore.GaussDB/Diagnostics/Internal/GaussDBLoggingDefinitions.cs @@ -0,0 +1,146 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.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 GaussDBLoggingDefinitions : RelationalLoggingDefinitions +{ + /// + /// 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 EventDefinitionBase? LogFoundDefaultSchema; + + /// + /// 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 EventDefinitionBase? LogFoundColumn; + + /// + /// 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 EventDefinitionBase? LogFoundForeignKey; + + /// + /// 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 EventDefinitionBase? LogFoundCollation; + + /// + /// 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 EventDefinitionBase? LogPrincipalTableNotInSelectionSet; + + /// + /// 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 EventDefinitionBase? LogMissingSchema; + + /// + /// 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 EventDefinitionBase? LogMissingTable; + + /// + /// 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 EventDefinitionBase? LogFoundSequence; + + /// + /// 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 EventDefinitionBase? LogFoundTable; + + /// + /// 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 EventDefinitionBase? LogFoundIndex; + + /// + /// 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 EventDefinitionBase? LogFoundPrimaryKey; + + /// + /// 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 EventDefinitionBase? LogFoundUniqueConstraint; + + /// + /// 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 EventDefinitionBase? LogPrincipalColumnNotFound; + + /// + /// 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 EventDefinitionBase? LogEnumColumnSkipped; + + /// + /// 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 EventDefinitionBase? LogExpressionIndexSkipped; + + /// + /// 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 EventDefinitionBase? LogUnsupportedColumnConstraintSkipped; + + /// + /// 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 EventDefinitionBase? LogUnsupportedColumnIndexSkipped; +} diff --git a/src/EFCore.GaussDB/EFCore.GaussDB.csproj b/src/EFCore.GaussDB/EFCore.GaussDB.csproj new file mode 100644 index 0000000000..39c2cbda96 --- /dev/null +++ b/src/EFCore.GaussDB/EFCore.GaussDB.csproj @@ -0,0 +1,39 @@ + + + + HuaweiCloud.EntityFrameworkCore.GaussDB + HuaweiCloud.EntityFrameworkCore.GaussDB + + Conan Yao + GaussDB provider for Entity Framework Core. + gaussdb;opengauss;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql + README.md + EF9100 + https://github.com/HuaweiCloudDeveloper/gaussdb-efcore + Copyright 2026 © The HuaweiCloud Development Team + + + + + + + + + + + + + + + + + + TextTemplatingFileGenerator + + + + + + + + diff --git a/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBEntityTypeBuilderExtensions.cs b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBEntityTypeBuilderExtensions.cs new file mode 100644 index 0000000000..0f1307d4bd --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBEntityTypeBuilderExtensions.cs @@ -0,0 +1,298 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.GaussDBTypes; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// GaussDB-specific extension methods for . +/// +/// +/// See Modeling entity types and relationships. +/// +public static class GaussDBEntityTypeBuilderExtensions +{ + #region Generated tsvector column + + // Note: actual configuration for generated TsVector properties is on the property + + /// + /// Configures a property on this entity to be a full-text search tsvector column over other given properties. + /// + /// The builder for the entity being configured. + /// + /// A lambda expression representing the property to be configured as a tsvector column + /// (blog => blog.Url). + /// + /// + /// + /// 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. + /// + /// + /// + /// + /// A lambda expression representing the property(s) to be included in the tsvector column + /// (blog => blog.Url). + /// + /// + /// If multiple properties are to be included then specify an anonymous type including the + /// properties (post => new { post.Title, post.BlogId }). + /// + /// + /// A builder to further configure the property. + public static EntityTypeBuilder HasGeneratedTsVectorColumn( + this EntityTypeBuilder entityTypeBuilder, + Expression> tsVectorPropertyExpression, + string config, + Expression> includeExpression) + where TEntity : class + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + Check.NotNull(tsVectorPropertyExpression, nameof(tsVectorPropertyExpression)); + Check.NotNull(config, nameof(config)); + Check.NotNull(includeExpression, nameof(includeExpression)); + + entityTypeBuilder.Property(tsVectorPropertyExpression).IsGeneratedTsVectorColumn( + config, + includeExpression.GetPropertyAccessList().Select(EntityFrameworkMemberInfoExtensions.GetSimpleMemberName).ToArray()); + + return entityTypeBuilder; + } + + #endregion Generated tsvector column + + #region Storage parameters + + /// + /// Sets a GaussDB storage parameter on the table created for this entity. + /// + /// + /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS + /// + /// The builder for the entity type being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder HasStorageParameter( + this EntityTypeBuilder entityTypeBuilder, + string parameterName, + object? parameterValue) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + entityTypeBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); + + return entityTypeBuilder; + } + + /// + /// Sets a GaussDB storage parameter on the table created for this entity. + /// + /// + /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS + /// + /// The builder for the entity type being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder HasStorageParameter( + this EntityTypeBuilder entityTypeBuilder, + string parameterName, + object? parameterValue) + where TEntity : class + => (EntityTypeBuilder)HasStorageParameter((EntityTypeBuilder)entityTypeBuilder, parameterName, parameterValue); + + /// + /// Sets a GaussDB storage parameter on the table created for this entity. + /// + /// + /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS + /// + /// The builder for the entity type being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance so that multiple calls can be chained. + public static IConventionEntityTypeBuilder? HasStorageParameter( + this IConventionEntityTypeBuilder entityTypeBuilder, + string parameterName, + object? parameterValue, + bool fromDataAnnotation = false) + { + if (entityTypeBuilder.CanSetStorageParameter(parameterName, parameterValue, fromDataAnnotation)) + { + entityTypeBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); + + return entityTypeBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the GaussDB storage parameter is set on the table created for this entity. + /// + /// + /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS + /// + /// The builder for the entity type being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the mapped table can be configured as with the storage parameter. + public static bool CanSetStorageParameter( + this IConventionEntityTypeBuilder entityTypeBuilder, + string parameterName, + object? parameterValue, + bool fromDataAnnotation = false) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + return entityTypeBuilder.CanSetAnnotation( + GaussDBAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); + } + + #endregion Storage parameters + + #region Unlogged Table + + /// + /// Configures the entity to use an unlogged table when targeting GaussDB. + /// + /// The builder for the entity type being configured. + /// True to configure the entity to use an unlogged table; otherwise, false. + /// + /// The same builder instance so that multiple calls can be chained. + /// + /// + /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED + /// + public static EntityTypeBuilder IsUnlogged( + this EntityTypeBuilder entityTypeBuilder, + bool unlogged = true) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + entityTypeBuilder.Metadata.SetIsUnlogged(unlogged); + + return entityTypeBuilder; + } + + /// + /// Configures the mapped table to use an unlogged table when targeting GaussDB. + /// + /// The builder for the entity type being configured. + /// True to configure the entity to use an unlogged table; otherwise, false. + /// + /// The same builder instance so that multiple calls can be chained. + /// + /// + /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED + /// + public static EntityTypeBuilder IsUnlogged( + this EntityTypeBuilder entityTypeBuilder, + bool unlogged = true) + where TEntity : class + => (EntityTypeBuilder)IsUnlogged((EntityTypeBuilder)entityTypeBuilder, unlogged); + + /// + /// Configures the mapped table to use an unlogged table when targeting GaussDB. + /// + /// The builder for the entity type being configured. + /// True to configure the entity to use an unlogged table; otherwise, false. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance so that multiple calls can be chained. + /// + /// + /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED + /// + public static IConventionEntityTypeBuilder? IsUnlogged( + this IConventionEntityTypeBuilder entityTypeBuilder, + bool unlogged = true, + bool fromDataAnnotation = false) + { + if (entityTypeBuilder.CanSetIsUnlogged(unlogged, fromDataAnnotation)) + { + entityTypeBuilder.Metadata.SetIsUnlogged(unlogged, fromDataAnnotation); + + return entityTypeBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the mapped table can be configured to use an unlogged table when targeting GaussDB. + /// + /// The builder for the entity type being configured. + /// True to configure the entity to use an unlogged table; otherwise, false. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance so that multiple calls can be chained. + /// + /// + /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED + /// + public static bool CanSetIsUnlogged( + this IConventionEntityTypeBuilder entityTypeBuilder, + bool unlogged = true, + bool fromDataAnnotation = false) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + return entityTypeBuilder.CanSetAnnotation(GaussDBAnnotationNames.UnloggedTable, unlogged, fromDataAnnotation); + } + + #endregion + + #region CockroachDB Interleave-in-parent + + /// + /// Specifies that the CockroachDB-specific "interleave in parent" feature should be used. + /// + public static EntityTypeBuilder UseCockroachDbInterleaveInParent( + this EntityTypeBuilder entityTypeBuilder, + Type parentTableType, + List interleavePrefix) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + Check.NotNull(parentTableType, nameof(parentTableType)); + Check.NotNull(interleavePrefix, nameof(interleavePrefix)); + + var parentEntity = entityTypeBuilder.Metadata.Model.FindEntityType(parentTableType); + if (parentEntity is null) + { + throw new ArgumentException($"Entity not found in model for type: {parentEntity}", nameof(parentTableType)); + } + + if (StoreObjectIdentifier.Create(parentEntity, StoreObjectType.Table) is not { } tableIdentifier) + { + throw new ArgumentException($"Entity {parentEntity.DisplayName()} is not mapped to a database table"); + } + + var interleaveInParent = entityTypeBuilder.Metadata.GetCockroachDbInterleaveInParent(); + interleaveInParent.ParentTableSchema = tableIdentifier.Schema; + interleaveInParent.ParentTableName = tableIdentifier.Name; + interleaveInParent.InterleavePrefix = interleavePrefix; + + return entityTypeBuilder; + } + + /// + /// Specifies that the CockroachDB-specific "interleave in parent" feature should be used. + /// + public static EntityTypeBuilder UseCockroachDbInterleaveInParent( + this EntityTypeBuilder entityTypeBuilder, + Type parentTableType, + List interleavePrefix) + where TEntity : class + => (EntityTypeBuilder)UseCockroachDbInterleaveInParent( + (EntityTypeBuilder)entityTypeBuilder, parentTableType, interleavePrefix); + + #endregion CockroachDB Interleave-in-parent +} diff --git a/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBIndexBuilderExtensions.cs b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBIndexBuilderExtensions.cs new file mode 100644 index 0000000000..ce9ee7633b --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBIndexBuilderExtensions.cs @@ -0,0 +1,850 @@ +using System.Collections; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// GaussDB specific extension methods for . +/// +public static class GaussDBIndexBuilderExtensions +{ + #region Method + + /// + /// The GaussDB 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. + /// A builder to further configure the index. + public static IndexBuilder HasMethod( + this IndexBuilder indexBuilder, + string? method) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(method, nameof(method)); + + indexBuilder.Metadata.SetMethod(method); + + return indexBuilder; + } + + /// + /// The GaussDB 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. + /// A builder to further configure the index. + public static IndexBuilder HasMethod( + this IndexBuilder indexBuilder, + string? method) + => (IndexBuilder)HasMethod((IndexBuilder)indexBuilder, method); + + /// + /// The GaussDB 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. + /// A builder to further configure the index. + public static IConventionIndexBuilder? HasMethod( + this IConventionIndexBuilder indexBuilder, + string? method, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetMethod(method, fromDataAnnotation)) + { + indexBuilder.Metadata.SetMethod(method, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// The GaussDB 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 + public static bool CanSetMethod( + this IConventionIndexBuilder indexBuilder, + string? method, + bool fromDataAnnotation = false) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(GaussDBAnnotationNames.IndexMethod, method, fromDataAnnotation); + } + + #endregion Method + + #region Operators + + /// + /// The GaussDB index operators to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + /// The builder for the index being configured. + /// The operators to use for each column. + /// A builder to further configure the index. + public static IndexBuilder HasOperators( + this IndexBuilder indexBuilder, + params string[]? operators) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(operators, nameof(operators)); + + indexBuilder.Metadata.SetOperators(operators); + + return indexBuilder; + } + + /// + /// The GaussDB index operators to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + /// The builder for the index being configured. + /// The operators to use for each column. + /// A builder to further configure the index. + public static IndexBuilder HasOperators( + this IndexBuilder indexBuilder, + params string[]? operators) + => (IndexBuilder)HasOperators((IndexBuilder)indexBuilder, operators); + + /// + /// The GaussDB index operators to be used. + /// + /// + /// 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. + /// A builder to further configure the index. + public static IConventionIndexBuilder? HasOperators( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? operators, + bool fromDataAnnotation) + { + if (indexBuilder.CanSetOperators(operators, fromDataAnnotation)) + { + indexBuilder.Metadata.SetOperators(operators, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the GaussDB 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. + public static bool CanSetOperators( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? operators, + bool fromDataAnnotation) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(GaussDBAnnotationNames.IndexOperators, operators, fromDataAnnotation); + } + + #endregion Operators + + #region IsTsVectorExpressionIndex + + /// + /// Configures this index to be 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. + /// + /// + /// A builder to further configure the index. + public static IndexBuilder IsTsVectorExpressionIndex( + this IndexBuilder indexBuilder, + string config) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NotNull(config, nameof(config)); + + indexBuilder.Metadata.SetTsVectorConfig(config); + return indexBuilder; + } + + /// + /// Configures this index to be 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. + /// + /// + /// A builder to further configure the index. + public static IndexBuilder IsTsVectorExpressionIndex( + this IndexBuilder indexBuilder, + string config) + => (IndexBuilder)IsTsVectorExpressionIndex((IndexBuilder)indexBuilder, config); + + /// + /// Configures this index to be 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. + /// + /// + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + public static IConventionIndexBuilder? IsTsVectorExpressionIndex( + this IConventionIndexBuilder indexBuilder, + string? config) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + if (indexBuilder.CanSetIsTsVectorExpressionIndex(config)) + { + indexBuilder.Metadata.SetTsVectorConfig(config); + + return indexBuilder; + } + + return null; + } + + /// + /// 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. + public static bool CanSetIsTsVectorExpressionIndex( + this IConventionIndexBuilder indexBuilder, + string? config, + bool fromDataAnnotation = false) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(GaussDBAnnotationNames.TsVectorConfig, config, fromDataAnnotation); + } + + #endregion IsTsVectorExpressionIndex + + #region Collation + + /// + /// The GaussDB 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. + public static IndexBuilder UseCollation( + this IndexBuilder indexBuilder, + params string[]? values) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(values, nameof(values)); + + indexBuilder.Metadata.SetCollation(values); + + return indexBuilder; + } + + /// + /// The GaussDB 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. + public static IndexBuilder UseCollation( + this IndexBuilder indexBuilder, + params string[]? values) + => (IndexBuilder)UseCollation((IndexBuilder)indexBuilder, values); + + /// + /// The GaussDB 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. + public static IConventionIndexBuilder? UseCollation( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? values, + bool fromDataAnnotation) + { + if (indexBuilder.CanSetCollation(values, fromDataAnnotation)) + { + indexBuilder.Metadata.SetCollation(values, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the GaussDB 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. + public static bool CanSetCollation( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? values, + bool fromDataAnnotation) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(RelationalAnnotationNames.Collation, values, fromDataAnnotation); + } + + #endregion Collation + + #region Null sort order + + /// + /// The GaussDB index NULL 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. + public static IndexBuilder HasNullSortOrder( + this IndexBuilder indexBuilder, + params NullSortOrder[]? values) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(values, nameof(values)); + + if (!SortOrderHelper.IsDefaultNullSortOrder(values, indexBuilder.Metadata.IsDescending)) + { + indexBuilder.Metadata.SetNullSortOrder(values); + } + + return indexBuilder; + } + + /// + /// The GaussDB index NULL 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. + public static IndexBuilder HasNullSortOrder( + this IndexBuilder indexBuilder, + params NullSortOrder[]? values) + => (IndexBuilder)HasNullSortOrder((IndexBuilder)indexBuilder, values); + + /// + /// The GaussDB index NULL 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. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the index. + public static IConventionIndexBuilder? HasNullSortOrder( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? values, + bool fromDataAnnotation) + { + if (indexBuilder.CanSetNullSortOrder(values, fromDataAnnotation)) + { + if (!SortOrderHelper.IsDefaultNullSortOrder(values, indexBuilder.Metadata.IsDescending)) + { + indexBuilder.Metadata.SetNullSortOrder(values, fromDataAnnotation); + } + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the GaussDB 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. + public static bool CanSetNullSortOrder( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? values, + bool fromDataAnnotation) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(GaussDBAnnotationNames.IndexNullSortOrder, values, fromDataAnnotation); + } + + #endregion Null sort order + + #region Include + + /// + /// Adds an INCLUDE clause to the index definition with the specified property names. + /// This clause specifies a list of columns which will be included as a non-key part in the index. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// An array of property names to be used in INCLUDE clause. + /// A builder to further configure the index. + public static IndexBuilder IncludeProperties( + this IndexBuilder indexBuilder, + params string[] propertyNames) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(propertyNames, nameof(propertyNames)); + + indexBuilder.Metadata.SetIncludeProperties(propertyNames); + + return indexBuilder; + } + + /// + /// Adds an INCLUDE clause to the index definition with the specified property names. + /// This clause specifies a list of columns which will be included as a non-key part in the index. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// An array of property names to be used in INCLUDE clause. + /// A builder to further configure the index. + public static IndexBuilder IncludeProperties( + this IndexBuilder indexBuilder, + params string[] propertyNames) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(propertyNames, nameof(propertyNames)); + + indexBuilder.Metadata.SetIncludeProperties(propertyNames); + + return indexBuilder; + } + + /// + /// Adds an INCLUDE clause to the index definition with property names from the specified expression. + /// This clause specifies a list of columns which will be included as a non-key part in the index. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// + /// + /// A lambda expression representing the property(s) to be included in the INCLUDE clause + /// (blog => blog.Url). + /// + /// + /// If multiple properties are to be included then specify an anonymous type including the + /// properties (post => new { post.Title, post.BlogId }). + /// + /// + /// A builder to further configure the index. + public static IndexBuilder IncludeProperties( + this IndexBuilder indexBuilder, + Expression> includeExpression) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NotNull(includeExpression, nameof(includeExpression)); + + indexBuilder.IncludeProperties(includeExpression.GetPropertyAccessList().Select(x => x.Name).ToArray()); + + return indexBuilder; + } + + /// + /// Adds an INCLUDE clause to the index definition with the specified property names. + /// This clause specifies a list of columns which will be included as a non-key part in the index. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// 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. + /// A builder to further configure the index. + public static IConventionIndexBuilder? IncludeProperties( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList propertyNames, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetIncludeProperties(propertyNames, fromDataAnnotation)) + { + indexBuilder.Metadata.SetIncludeProperties(propertyNames, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// 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. + public static bool CanSetIncludeProperties( + this IConventionIndexBuilder indexBuilder, + IReadOnlyList? propertyNames, + bool fromDataAnnotation = false) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + .Overrides(indexBuilder.Metadata.GetIncludePropertiesConfigurationSource()) + || StructuralComparisons.StructuralEqualityComparer.Equals( + propertyNames, indexBuilder.Metadata.GetIncludeProperties()); + } + + #endregion Include + + #region Created concurrently + + /// + /// When this option is used, GaussDB will build the index without taking any locks that prevent concurrent inserts, + /// updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with the "concurrently" option. + /// A builder to further configure the index. + public static IndexBuilder IsCreatedConcurrently(this IndexBuilder indexBuilder, bool createdConcurrently = true) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + indexBuilder.Metadata.SetIsCreatedConcurrently(createdConcurrently); + + return indexBuilder; + } + + /// + /// When this option is used, GaussDB will build the index without taking any locks that prevent concurrent inserts, + /// updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with the "concurrently" option. + /// A builder to further configure the index. + public static IndexBuilder IsCreatedConcurrently( + this IndexBuilder indexBuilder, + bool createdConcurrently = true) + => (IndexBuilder)IsCreatedConcurrently((IndexBuilder)indexBuilder, createdConcurrently); + + /// + /// When this option is used, GaussDB will build the index without taking any locks that prevent concurrent inserts, + /// updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with the "concurrently" option. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the index. + public static IConventionIndexBuilder? IsCreatedConcurrently( + this IConventionIndexBuilder indexBuilder, + bool? createdConcurrently, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetIsCreatedConcurrently(createdConcurrently, fromDataAnnotation)) + { + indexBuilder.Metadata.SetIsCreatedConcurrently(createdConcurrently); + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether concurrent creation for the index can be set. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with the "concurrently" option. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the index. + public static bool CanSetIsCreatedConcurrently( + this IConventionIndexBuilder indexBuilder, + bool? createdConcurrently, + bool fromDataAnnotation = false) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(GaussDBAnnotationNames.CreatedConcurrently, createdConcurrently, fromDataAnnotation); + } + + #endregion Created concurrently + + #region NULLS distinct + + /// + /// Specifies whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// Whether nulls should be considered distinct. + /// A builder to further configure the index. + public static IndexBuilder AreNullsDistinct( + this IndexBuilder indexBuilder, + bool nullsDistinct = true) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + indexBuilder.Metadata.SetAreNullsDistinct(nullsDistinct); + + return indexBuilder; + } + + /// + /// Specifies whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// Whether nulls should be considered distinct. + /// A builder to further configure the index. + public static IndexBuilder AreNullsDistinct( + this IndexBuilder indexBuilder, + bool nullsDistinct = true) + => (IndexBuilder)AreNullsDistinct((IndexBuilder)indexBuilder, nullsDistinct); + + /// + /// Specifies whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// Whether nulls should be considered distinct. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the index. + public static IConventionIndexBuilder? AreNullsDistinct( + this IConventionIndexBuilder indexBuilder, + bool nullsDistinct = true, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetAreNullsDistinct(nullsDistinct, fromDataAnnotation)) + { + indexBuilder.Metadata.SetAreNullsDistinct(nullsDistinct, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// Specifies whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// https://www.postgresql.org/docs/current/sql-createindex.html + /// + /// The builder for the index being configured. + /// Whether nulls should be considered distinct. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the index can be configured with the method + public static bool CanSetAreNullsDistinct( + this IConventionIndexBuilder indexBuilder, + bool nullsDistinct = true, + bool fromDataAnnotation = false) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation(GaussDBAnnotationNames.NullsDistinct, nullsDistinct, fromDataAnnotation); + } + + #endregion NULLS distinct + + #region Storage parameters + + /// + /// Sets a GaussDB storage parameter on the index. + /// + /// + /// See https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS + /// + /// The builder for the index being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// The same builder instance so that multiple calls can be chained. + public static IndexBuilder HasStorageParameter( + this IndexBuilder indexBuilder, + string parameterName, + object? parameterValue) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + indexBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); + + return indexBuilder; + } + + /// + /// Sets a GaussDB storage parameter on the index. + /// + /// + /// See https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS + /// + /// The builder for the index being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// The same builder instance so that multiple calls can be chained. + public static IndexBuilder HasStorageParameter( + this IndexBuilder indexBuilder, + string parameterName, + object? parameterValue) + where TEntity : class + => (IndexBuilder)HasStorageParameter((IndexBuilder)indexBuilder, parameterName, parameterValue); + + /// + /// Sets a GaussDB storage parameter on the index. + /// + /// + /// See https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS + /// + /// The builder for the index being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the index can be configured with the method + public static IConventionIndexBuilder? HasStorageParameter( + this IConventionIndexBuilder indexBuilder, + string parameterName, + object? parameterValue, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetStorageParameter(parameterName, parameterValue, fromDataAnnotation)) + { + indexBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the GaussDB storage parameter is set on the table created for this entity. + /// + /// + /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS + /// + /// The builder for the index being configured. + /// The name of the storage parameter. + /// The value of the storage parameter. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the index can be configured as with the storage parameter. + public static bool CanSetStorageParameter( + this IConventionIndexBuilder indexBuilder, + string parameterName, + object? parameterValue, + bool fromDataAnnotation = false) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + return indexBuilder.CanSetAnnotation( + GaussDBAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); + } + + #endregion Storage parameters +} diff --git a/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBModelBuilderExtensions.cs b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBModelBuilderExtensions.cs new file mode 100644 index 0000000000..67960a1e8e --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBModelBuilderExtensions.cs @@ -0,0 +1,789 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDB.NameTranslation; +using HuaweiCloud.GaussDBTypes; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// GaussDB-specific extension methods for . +/// +public static class GaussDBModelBuilderExtensions +{ + #region HiLo + + /// + /// Configures the model to use a sequence-based hi-lo pattern to generate values for properties + /// marked as , when targeting GaussDB. + /// + /// The model builder. + /// The name of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseHiLo(this ModelBuilder modelBuilder, string? name = null, string? schema = null) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var model = modelBuilder.Model; + + name ??= GaussDBModelExtensions.DefaultHiLoSequenceName; + + if (model.FindSequence(name, schema) is null) + { + modelBuilder.HasSequence(name, schema).IncrementsBy(10); + } + + model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + model.SetHiLoSequenceName(name); + model.SetHiLoSequenceSchema(schema); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); + + return modelBuilder; + } + + /// + /// Configures the database sequence used for the hi-lo pattern to generate values for key properties + /// marked as , when targeting GaussDB. + /// + /// The model builder. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the sequence. + public static IConventionSequenceBuilder? HasHiLoSequence( + this IConventionModelBuilder modelBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + if (!modelBuilder.CanSetHiLoSequence(name, schema)) + { + return null; + } + + modelBuilder.Metadata.SetHiLoSequenceName(name, fromDataAnnotation); + modelBuilder.Metadata.SetHiLoSequenceSchema(schema, fromDataAnnotation); + + return name is null ? null : modelBuilder.HasSequence(name, schema, fromDataAnnotation); + } + + /// + /// Returns a value indicating whether the given name and schema can be set for the hi-lo sequence. + /// + /// The model builder. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given name and schema can be set for the hi-lo sequence. + public static bool CanSetHiLoSequence( + this IConventionModelBuilder modelBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + return modelBuilder.CanSetAnnotation(GaussDBAnnotationNames.HiLoSequenceName, name, fromDataAnnotation) + && modelBuilder.CanSetAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema, schema, fromDataAnnotation); + } + + #endregion HiLo + + #region Serial + + /// + /// + /// Configures the model to use the GaussDB SERIAL feature to generate values for properties + /// marked as , when targeting GaussDB. + /// + /// + /// This option should be considered deprecated starting with GaussDB 10, consider using instead. + /// + /// + /// The model builder. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseSerialColumns( + this ModelBuilder modelBuilder) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + var property = modelBuilder.Model; + + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SerialColumn); + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + + return modelBuilder; + } + + #endregion Serial + + #region Identity + + /// + /// + /// Configures the model to use the GaussDB IDENTITY feature to generate values for properties + /// marked as , when targeting GaussDB. Values for these + /// columns will always be generated as identity, and the application will not be able to override + /// this behavior by providing a value. + /// + /// Available only starting GaussDB 10. + /// + /// The model builder. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseIdentityAlwaysColumns(this ModelBuilder modelBuilder) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + var model = modelBuilder.Model; + + model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.IdentityAlwaysColumn); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); + + return modelBuilder; + } + + /// + /// + /// Configures the model to use the GaussDB IDENTITY feature to generate values for properties + /// marked as , when targeting GaussDB. Values for these + /// columns will be generated as identity by default, but the application will be able to override + /// this behavior by providing a value. + /// + /// + /// This is the default behavior when targeting GaussDB. Available only starting GaussDB 10. + /// + /// + /// The model builder. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseIdentityByDefaultColumns(this ModelBuilder modelBuilder) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + var model = modelBuilder.Model; + + model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.IdentityByDefaultColumn); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); + + return modelBuilder; + } + + /// + /// + /// Configures the model to use the GaussDB IDENTITY feature to generate values for properties + /// marked as , when targeting GaussDB. Values for these + /// columns will be generated as identity by default, but the application will be able to override + /// this behavior by providing a value. + /// + /// + /// This is the default behavior when targeting GaussDB. Available only starting GaussDB 10. + /// + /// + /// The model builder. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseIdentityColumns(this ModelBuilder modelBuilder) + => modelBuilder.UseIdentityByDefaultColumns(); + + /// + /// Configures the value generation strategy for the key property, when targeting GaussDB. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, null otherwise. + /// + public static IConventionModelBuilder? HasValueGenerationStrategy( + this IConventionModelBuilder modelBuilder, + GaussDBValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + if (modelBuilder.CanSetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation)) + { + modelBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); + + if (valueGenerationStrategy != GaussDBValueGenerationStrategy.SequenceHiLo) + { + modelBuilder.HasHiLoSequence(null, null, fromDataAnnotation); + } + + if (valueGenerationStrategy != GaussDBValueGenerationStrategy.Sequence) + { + if (modelBuilder.CanSetAnnotation(GaussDBAnnotationNames.SequenceNameSuffix, null) + && modelBuilder.CanSetAnnotation(GaussDBAnnotationNames.SequenceSchema, null)) + { + modelBuilder.Metadata.SetSequenceNameSuffix(null, fromDataAnnotation); + modelBuilder.Metadata.SetSequenceSchema(null, fromDataAnnotation); + } + } + + return modelBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the given value can be set as the default value generation strategy. + /// + /// The model builder. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given value can be set as the default value generation strategy. + public static bool CanSetValueGenerationStrategy( + this IConventionModelBuilder modelBuilder, + GaussDBValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + return modelBuilder.CanSetAnnotation( + GaussDBAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); + } + + #endregion Identity + + #region Sequence + + /// + /// Configures the model to use a sequence per hierarchy to generate values for key properties marked as + /// , when targeting GaussDB. + /// + /// The model builder. + /// The name that will suffix the table name for each sequence created automatically. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseKeySequences( + this ModelBuilder modelBuilder, + string? nameSuffix = null, + string? schema = null) + { + Check.NullButNotEmpty(nameSuffix, nameof(nameSuffix)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var model = modelBuilder.Model; + + nameSuffix ??= GaussDBModelExtensions.DefaultSequenceNameSuffix; + + model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.Sequence); + model.SetSequenceNameSuffix(nameSuffix); + model.SetSequenceSchema(schema); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); + + return modelBuilder; + } + + #endregion Sequence + + #region Extensions + + /// + /// Registers a GaussDB extension in the model. + /// + /// The model builder in which to define the extension. + /// The schema in which to create the extension. + /// The name of the extension to create. + /// The version of the extension. + /// The same builder instance so that multiple calls can be chained. + /// + /// See: https://www.postgresql.org/docs/current/external-extensions.html + /// + /// + /// + /// + public static ModelBuilder HasPostgresExtension( + this ModelBuilder modelBuilder, + string? schema, + string name, + string? version = null) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + modelBuilder.Model.GetOrAddPostgresExtension(schema, name, version); + + return modelBuilder; + } + + /// + /// Registers a GaussDB extension in the model. + /// + /// The model builder in which to define the extension. + /// The name of the extension to create. + /// The same builder instance so that multiple calls can be chained. + /// + /// See: https://www.postgresql.org/docs/current/external-extensions.html + /// + /// + /// + /// + public static ModelBuilder HasPostgresExtension( + this ModelBuilder modelBuilder, + string name) + => modelBuilder.HasPostgresExtension(null, name); + + /// + /// Registers a GaussDB extension in the model. + /// + /// The model builder in which to define the extension. + /// The schema in which to create the extension. + /// The name of the extension to create. + /// The version of the extension. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance so that multiple calls can be chained. + /// + /// See: https://www.postgresql.org/docs/current/external-extensions.html + /// + /// + /// + /// + public static IConventionModelBuilder? HasPostgresExtension( + this IConventionModelBuilder modelBuilder, + string? schema, + string name, + string? version = null, + bool fromDataAnnotation = false) + { + if (modelBuilder.CanSetPostgresExtension(schema, name, version, fromDataAnnotation)) + { + modelBuilder.Metadata.GetOrAddPostgresExtension(schema, name, version); + return modelBuilder; + } + + return null; + } + + /// + /// Registers a GaussDB extension in the model. + /// + /// The model builder in which to define the extension. + /// The name of the extension to create. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance so that multiple calls can be chained. + /// + /// See: https://www.postgresql.org/docs/current/external-extensions.html + /// + /// + /// + /// + public static IConventionModelBuilder? HasPostgresExtension( + this IConventionModelBuilder modelBuilder, + string name, + bool fromDataAnnotation = false) + => modelBuilder.HasPostgresExtension(schema: null, name, version: null, fromDataAnnotation); + + /// + /// Returns a value indicating whether the given GaussDB 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. + /// The version of the extension. + /// 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 CanSetPostgresExtension( + this IConventionModelBuilder modelBuilder, + string? schema, + string name, + string? version = null, + bool fromDataAnnotation = false) + { + var annotationName = GaussDBExtension.BuildAnnotationName(schema, name); + + return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name},{version}", fromDataAnnotation); + } + + #endregion + + #region Enums + + /// + /// 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 ModelBuilder HasPostgresEnum( + this ModelBuilder modelBuilder, + string? schema, + string name, + string[] labels) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(name, nameof(name)); + Check.NotNull(labels, nameof(labels)); + + modelBuilder.Model.GetOrAddPostgresEnum(schema, name, labels); + 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 GaussDB 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 = GaussDBExtension.BuildAnnotationName(schema, name); + + return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name}", fromDataAnnotation); + } + + /// + /// Registers a user-defined enum type in the model. + /// + /// The model builder 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 ModelBuilder HasPostgresEnum( + this ModelBuilder modelBuilder, + string name, + string[] labels) + => modelBuilder.HasPostgresEnum(null, name, labels); + + /// + /// 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 translator for name and label inference. + /// Defaults to . + /// + /// + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html + /// + /// builder + public static ModelBuilder HasPostgresEnum( + this ModelBuilder modelBuilder, + string? schema = null, + string? name = null, + IGaussDBNameTranslator? nameTranslator = null) + where TEnum : struct, Enum + { +#pragma warning disable CS0618 // GaussDBConnection.GlobalTypeMapper is obsolete + nameTranslator ??= GaussDBConnection.GlobalTypeMapper.DefaultNameTranslator; +#pragma warning restore CS0618 + + return modelBuilder.HasPostgresEnum( + schema, + name ?? GetTypePgName(nameTranslator), + GetMemberPgNames(nameTranslator)); + } + + #endregion + + #region Templates + + /// + /// Specifies the GaussDB database to use as a template when creating a new database for this model. + /// + public static ModelBuilder UseDatabaseTemplate(this ModelBuilder modelBuilder, string templateDatabaseName) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(templateDatabaseName, nameof(templateDatabaseName)); + + modelBuilder.Model.SetDatabaseTemplate(templateDatabaseName); + return modelBuilder; + } + + #endregion + + #region Ranges + + /// + /// Registers a user-defined range type in the model. + /// + /// The model builder on which to create the range type. + /// The schema in which to create the range type. + /// The name of the range type to be created. + /// The subtype (or element type) of the range + /// + /// An optional GaussDB function which converts range values to a canonical form. + /// + /// Used to specify a non-default operator class. + /// Used to specify a non-default collation in the range's order. + /// + /// An optional GaussDB function taking two values of the subtype type as argument, and return a double + /// precision value representing the difference between the two given values. + /// + /// + /// See https://www.postgresql.org/docs/current/static/rangetypes.html, + /// https://www.postgresql.org/docs/current/static/sql-createtype.html, + /// + public static ModelBuilder HasPostgresRange( + this ModelBuilder modelBuilder, + string? schema, + string name, + string subtype, + string? canonicalFunction = null, + string? subtypeOpClass = null, + string? collation = null, + string? subtypeDiff = null) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(subtype, nameof(subtype)); + + modelBuilder.Model.GetOrAddPostgresRange( + schema, + name, + subtype, + canonicalFunction, + subtypeOpClass, + collation, + subtypeDiff); + return modelBuilder; + } + + /// + /// Registers a user-defined range type in the model. + /// + /// The model builder on which to create the range type. + /// The name of the range type to be created. + /// The subtype (or element type) of the range + /// + /// See https://www.postgresql.org/docs/current/static/rangetypes.html, + /// https://www.postgresql.org/docs/current/static/sql-createtype.html, + /// + public static ModelBuilder HasPostgresRange( + this ModelBuilder modelBuilder, + string name, + string subtype) + => HasPostgresRange(modelBuilder, null, name, subtype); + + #endregion + + #region Tablespaces + + /// + /// Specifies the GaussDB tablespace in which to place the new database created for this model. + /// + public static ModelBuilder UseTablespace( + this ModelBuilder modelBuilder, + string tablespace) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(tablespace, nameof(tablespace)); + + modelBuilder.Model.SetTablespace(tablespace); + return modelBuilder; + } + + #endregion + + #region Collation management + + /// + /// Creates a new collation in the database. + /// + /// + /// See https://www.postgresql.org/docs/current/sql-createcollation.html. + /// + /// The model builder on which to create the collation. + /// The name of the collation to create. + /// Sets LC_COLLATE and LC_CTYPE at once. + /// + /// Specifies the provider to use for locale services associated with this collation. + /// The available choices depend on the operating system and build options. + /// + /// + /// Specifies whether the collation should use deterministic comparisons. + /// Defaults to true. + /// + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder HasCollation( + this ModelBuilder modelBuilder, + string name, + string locale, + string? provider = null, + bool? deterministic = null) + => modelBuilder.HasCollation(schema: null, name, locale, provider: provider, deterministic: deterministic); + + /// + /// Creates a new collation in the database. + /// + /// + /// See https://www.postgresql.org/docs/current/sql-createcollation.html. + /// + /// The model builder on which to create the collation. + /// The schema in which to create the collation, or null for the default schema. + /// The name of the collation to create. + /// Sets LC_COLLATE and LC_CTYPE at once. + /// + /// Specifies the provider to use for locale services associated with this collation. + /// The available choices depend on the operating system and build options. + /// + /// + /// Specifies whether the collation should use deterministic comparisons. + /// Defaults to true. + /// + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder HasCollation( + this ModelBuilder modelBuilder, + string? schema, + string name, + string locale, + string? provider = null, + bool? deterministic = null) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(locale, nameof(locale)); + + modelBuilder.Model.GetOrAddCollation( + schema, + name, + locale, + locale, + provider, + deterministic); + return modelBuilder; + } + + /// + /// Creates a new collation in the database. + /// + /// + /// See https://www.postgresql.org/docs/current/sql-createcollation.html. + /// + /// The model builder on which to create the collation. + /// The schema in which to create the collation, or null for the default schema. + /// The name of the collation to create. + /// Use the specified operating system locale for the LC_COLLATE locale category. + /// Use the specified operating system locale for the LC_CTYPE locale category. + /// + /// Specifies the provider to use for locale services associated with this collation. + /// The available choices depend on the operating system and build options. + /// + /// + /// Specifies whether the collation should use deterministic comparisons. + /// Defaults to true. + /// + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder HasCollation( + this ModelBuilder modelBuilder, + string? schema, + string name, + string lcCollate, + string lcCtype, + string? provider = null, + bool? deterministic = null) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(lcCollate, nameof(lcCollate)); + Check.NotEmpty(lcCtype, nameof(lcCtype)); + + modelBuilder.Model.GetOrAddCollation( + schema, + name, + lcCollate, + lcCtype, + provider, + deterministic); + return modelBuilder; + } + + #endregion Collation management + + #region Helpers + + // See: https://github.com/npgsql/npgsql/blob/dev/src/GaussDB/TypeMapping/TypeMapperBase.cs#L132-L138 + private static string GetTypePgName(IGaussDBNameTranslator nameTranslator) + where TEnum : struct, Enum + => typeof(TEnum).GetCustomAttribute()?.PgName ?? nameTranslator.TranslateTypeName(typeof(TEnum).Name); + + // See: https://github.com/npgsql/npgsql/blob/dev/src/GaussDB/TypeHandlers/EnumHandler.cs#L118-L129 + private static string[] GetMemberPgNames(IGaussDBNameTranslator nameTranslator) + where TEnum : struct, Enum + => typeof(TEnum) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Select(x => x.GetCustomAttribute()?.PgName ?? nameTranslator.TranslateMemberName(x.Name)) + .ToArray(); + + #endregion +} diff --git a/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBPropertyBuilderExtensions.cs b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBPropertyBuilderExtensions.cs new file mode 100644 index 0000000000..4058e14049 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/BuilderExtensions/GaussDBPropertyBuilderExtensions.cs @@ -0,0 +1,915 @@ +using System.Collections; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.GaussDBTypes; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// GaussDB specific extension methods for . +/// +public static class GaussDBPropertyBuilderExtensions +{ + #region HiLo + + /// + /// Configures the property to use a sequence-based hi-lo pattern to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// + /// The builder for the property being configured. + /// The comment of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseHiLo( + this PropertyBuilder propertyBuilder, + string? name = null, + string? schema = null) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var property = propertyBuilder.Metadata; + + name ??= GaussDBModelExtensions.DefaultHiLoSequenceName; + + var model = property.DeclaringType.Model; + + if (model.FindSequence(name, schema) is null) + { + model.AddSequence(name, schema).IncrementBy = 10; + } + + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + property.SetHiLoSequenceName(name); + property.SetHiLoSequenceSchema(schema); + property.SetSequenceName(null); + property.SetSequenceSchema(null); + property.RemoveIdentityOptions(); + + return propertyBuilder; + } + + /// + /// Configures the property to use a sequence-based hi-lo pattern to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// + /// The builder for the property being configured. + /// The comment of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseHiLo( + this PropertyBuilder propertyBuilder, + string? name = null, + string? schema = null) + => (PropertyBuilder)UseHiLo((PropertyBuilder)propertyBuilder, name, schema); + + /// + /// Configures the database sequence used for the hi-lo pattern to generate values for the key property, + /// when targeting SQL Server. + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the sequence. + public static IConventionSequenceBuilder? HasHiLoSequence( + this IConventionPropertyBuilder propertyBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetHiLoSequence(name, schema)) + { + return null; + } + + propertyBuilder.Metadata.SetHiLoSequenceName(name, fromDataAnnotation); + propertyBuilder.Metadata.SetHiLoSequenceSchema(schema, fromDataAnnotation); + + return name is null + ? null + : propertyBuilder.Metadata.DeclaringType.Model.Builder.HasSequence(name, schema, fromDataAnnotation); + } + + /// + /// Returns a value indicating whether the given name and schema can be set for the hi-lo sequence. + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given name and schema can be set for the hi-lo sequence. + public static bool CanSetHiLoSequence( + this IConventionPropertyBuilder propertyBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + return propertyBuilder.CanSetAnnotation(GaussDBAnnotationNames.HiLoSequenceName, name, fromDataAnnotation) + && propertyBuilder.CanSetAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema, schema, fromDataAnnotation); + } + + #endregion HiLo + + #region Sequence + + /// + /// Configures the key property to use a sequence-based key value generation pattern to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseSequence( + this PropertyBuilder propertyBuilder, + string? name = null, + string? schema = null) + { + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var property = propertyBuilder.Metadata; + + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.Sequence); + property.SetSequenceName(name); + property.SetSequenceSchema(schema); + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + + return propertyBuilder; + } + + /// + /// Configures the key property to use a sequence-based key value generation pattern to generate values for new entities, + /// when targeting SQL Server. This method sets the property to be . + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseSequence( + this PropertyBuilder propertyBuilder, + string? name = null, + string? schema = null) + => (PropertyBuilder)UseSequence((PropertyBuilder)propertyBuilder, name, schema); + + /// + /// Configures the database sequence used for the key value generation pattern to generate values for the key property, + /// when targeting GaussDB. + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the sequence. + public static IConventionSequenceBuilder? HasSequence( + this IConventionPropertyBuilder propertyBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetSequence(name, schema, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetSequenceName(name, fromDataAnnotation); + propertyBuilder.Metadata.SetSequenceSchema(schema, fromDataAnnotation); + + return name == null + ? null + : propertyBuilder.Metadata.DeclaringType.Model.Builder.HasSequence(name, schema, fromDataAnnotation); + } + + /// + /// Returns a value indicating whether the given name and schema can be set for the key value generation sequence. + /// + /// The builder for the property being configured. + /// The name of the sequence. + /// The schema of the sequence. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given name and schema can be set for the key value generation sequence. + public static bool CanSetSequence( + this IConventionPropertyBuilder propertyBuilder, + string? name, + string? schema, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + return propertyBuilder.CanSetAnnotation(GaussDBAnnotationNames.SequenceName, name, fromDataAnnotation) + && propertyBuilder.CanSetAnnotation(GaussDBAnnotationNames.SequenceSchema, schema, fromDataAnnotation); + } + + #endregion Sequence + + #region Serial + + /// + /// Configures the property to use the GaussDB SERIAL feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// + /// + /// This option should be considered deprecated starting with GaussDB 10, consider using instead. + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseSerialColumn( + this PropertyBuilder propertyBuilder) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + var property = propertyBuilder.Metadata; + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SerialColumn); + property.SetSequenceName(null); + property.SetSequenceSchema(null); + property.RemoveHiLoOptions(); + property.RemoveIdentityOptions(); + + return propertyBuilder; + } + + /// + /// Configures the property to use the GaussDB SERIAL feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// + /// + /// This option should be considered deprecated starting with GaussDB 10, consider using instead. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseSerialColumn( + this PropertyBuilder propertyBuilder) + => (PropertyBuilder)UseSerialColumn((PropertyBuilder)propertyBuilder); + + #endregion Serial + + #region Identity always + + /// + /// + /// Configures the property to use the GaussDB IDENTITY feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// Values for this property will always be generated as identity, and the application will not be able + /// to override this behavior by providing a value. + /// + /// Available only starting GaussDB 10. + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseIdentityAlwaysColumn(this PropertyBuilder propertyBuilder) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + var property = propertyBuilder.Metadata; + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.IdentityAlwaysColumn); + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + property.SetSequenceName(null); + property.SetSequenceSchema(null); + + return propertyBuilder; + } + + /// + /// + /// Configures the property to use the GaussDB IDENTITY feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// Values for this property will always be generated as identity, and the application will not be able + /// to override this behavior by providing a value. + /// + /// Available only starting GaussDB 10. + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseIdentityAlwaysColumn( + this PropertyBuilder propertyBuilder) + => (PropertyBuilder)UseIdentityAlwaysColumn((PropertyBuilder)propertyBuilder); + + #endregion Identity always + + #region Identity by default + + /// + /// + /// Configures the property to use the GaussDB IDENTITY feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// Values for this property will be generated as identity by default, but the application will be able + /// to override this behavior by providing a value. + /// + /// + /// This is the default behavior when targeting GaussDB. Available only starting GaussDB 10. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseIdentityByDefaultColumn(this PropertyBuilder propertyBuilder) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + var property = propertyBuilder.Metadata; + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.IdentityByDefaultColumn); + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + property.SetSequenceName(null); + property.SetSequenceSchema(null); + + return propertyBuilder; + } + + /// + /// + /// Configures the property to use the GaussDB IDENTITY feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// Values for this property will be generated as identity by default, but the application will be able + /// to override this behavior by providing a value. + /// + /// + /// This is the default behavior when targeting GaussDB. Available only starting GaussDB 10. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseIdentityByDefaultColumn( + this PropertyBuilder propertyBuilder) + => (PropertyBuilder)UseIdentityByDefaultColumn((PropertyBuilder)propertyBuilder); + + /// + /// + /// Configures the property to use the GaussDB IDENTITY feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// Values for this property will be generated as identity by default, but the application will be able + /// to override this behavior by providing a value. + /// + /// + /// This internally calls . + /// This is the default behavior when targeting GaussDB. Available only starting GaussDB 10. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseIdentityColumn( + this PropertyBuilder propertyBuilder) + => propertyBuilder.UseIdentityByDefaultColumn(); + + /// + /// + /// Configures the property to use the GaussDB IDENTITY feature to generate values for new entities, + /// when targeting GaussDB. This method sets the property to be . + /// Values for this property will be generated as identity by default, but the application will be able + /// to override this behavior by providing a value. + /// + /// + /// This internally calls . + /// This is the default behavior when targeting GaussDB. Available only starting GaussDB 10. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder UseIdentityColumn( + this PropertyBuilder propertyBuilder) + => propertyBuilder.UseIdentityByDefaultColumn(); + + #endregion Identity by default + + #region General value generation strategy + + /// + /// Configures the value generation strategy for the key property, when targeting GaussDB. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, null otherwise. + /// + public static IConventionPropertyBuilder? HasValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + GaussDBValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + { + if (propertyBuilder.CanSetAnnotation( + GaussDBAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation)) + { + propertyBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); + + if (valueGenerationStrategy != GaussDBValueGenerationStrategy.SequenceHiLo) + { + propertyBuilder.HasHiLoSequence(null, null, fromDataAnnotation); + } + + if (valueGenerationStrategy != GaussDBValueGenerationStrategy.Sequence) + { + propertyBuilder.HasSequence(null, null, fromDataAnnotation); + } + + return propertyBuilder; + } + + 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, + GaussDBValueGenerationStrategy? valueGenerationStrategy, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => propertyBuilder.Metadata.FindOverrides(storeObject)?.Builder + .CanSetAnnotation( + GaussDBAnnotationNames.ValueGenerationStrategy, + valueGenerationStrategy, + fromDataAnnotation) + ?? true; + + /// + /// Returns a value indicating whether the given value can be set as the value generation strategy. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given value can be set as the default value generation strategy. + public static bool CanSetValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + GaussDBValueGenerationStrategy? valueGenerationStrategy, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation( + GaussDBAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); + + #endregion General value generation strategy + + #region Identity options + + /// + /// Sets the sequence options on an identity column. The column must be set as identity via + /// or . + /// + /// The builder for the property being configured. + /// + /// The starting value for the sequence. + /// The default starting value is for ascending sequences and for + /// descending ones. + /// + /// The amount to increment between values. Defaults to 1. + /// + /// The minimum value for the sequence. + /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. + /// + /// + /// The maximum value for the sequence. + /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. + /// + /// + /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. + /// Defaults to false. + /// + /// + /// Specifies how many sequence numbers are to be pre0allocated and stored in memory for faster access. + /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. + /// + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasIdentityOptions( + this PropertyBuilder propertyBuilder, + long? startValue = null, + long? incrementBy = null, + long? minValue = null, + long? maxValue = null, + bool? cyclic = null, + long? numbersToCache = null) + { + var property = propertyBuilder.Metadata; + property.SetIdentityStartValue(startValue); + property.SetIdentityIncrementBy(incrementBy); + property.SetIdentityMinValue(minValue); + property.SetIdentityMaxValue(maxValue); + property.SetIdentityIsCyclic(cyclic); + property.SetIdentityNumbersToCache(numbersToCache); + return propertyBuilder; + } + + /// + /// Sets the sequence options on an identity column. The column must be set as identity via + /// or . + /// + /// The builder for the property being configured. + /// + /// The starting value for the sequence. + /// The default starting value is for ascending sequences and for descending + /// ones. + /// + /// The amount to increment between values. Defaults to 1. + /// + /// The minimum value for the sequence. + /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. + /// + /// + /// The maximum value for the sequence. + /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. + /// + /// + /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. + /// Defaults to false. + /// + /// + /// Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. + /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. + /// + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasIdentityOptions( + this PropertyBuilder propertyBuilder, + long? startValue = null, + long? incrementBy = null, + long? minValue = null, + long? maxValue = null, + bool? cyclic = null, + long? numbersToCache = null) + => (PropertyBuilder)HasIdentityOptions( + (PropertyBuilder)propertyBuilder, startValue, incrementBy, minValue, maxValue, cyclic, numbersToCache); + + /// + /// Sets the sequence options on an identity column. The column must be set as identity via + /// or . + /// + /// The builder for the property being configured. + /// + /// The starting value for the sequence. + /// The default starting value is for ascending sequences and for descending + /// ones. + /// + /// The amount to increment between values. Defaults to 1. + /// + /// The minimum value for the sequence. + /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. + /// + /// + /// The maximum value for the sequence. + /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. + /// + /// + /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. + /// Defaults to false. + /// + /// + /// Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. + /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. + /// + /// The same builder instance so that multiple calls can be chained. + public static IConventionPropertyBuilder? HasIdentityOptions( + this IConventionPropertyBuilder propertyBuilder, + long? startValue = null, + long? incrementBy = null, + long? minValue = null, + long? maxValue = null, + bool? cyclic = null, + long? numbersToCache = null) + { + if (propertyBuilder.CanSetIdentityOptions(startValue, incrementBy, minValue, maxValue, cyclic, numbersToCache)) + { + var property = propertyBuilder.Metadata; + property.SetIdentityStartValue(startValue); + property.SetIdentityIncrementBy(incrementBy); + property.SetIdentityMinValue(minValue); + property.SetIdentityMaxValue(maxValue); + property.SetIdentityIsCyclic(cyclic); + property.SetIdentityNumbersToCache(numbersToCache); + return propertyBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the sequence options can be set on the identity column. + /// + /// The builder for the property being configured. + /// + /// The starting value for the sequence. The default starting value is for ascending sequences and + /// for descending ones. + /// + /// The amount to increment between values. Defaults to 1. + /// + /// The minimum value for the sequence. + /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. + /// + /// + /// The maximum value for the sequence. + /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. + /// + /// + /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. + /// Defaults to false. + /// + /// + /// Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. + /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. + /// + /// The same builder instance so that multiple calls can be chained. + public static bool CanSetIdentityOptions( + this IConventionPropertyBuilder propertyBuilder, + long? startValue = null, + long? incrementBy = null, + long? minValue = null, + long? maxValue = null, + bool? cyclic = null, + long? numbersToCache = null) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + var value = new IdentitySequenceOptionsData + { + StartValue = startValue, + IncrementBy = incrementBy ?? 1, + MinValue = minValue, + MaxValue = maxValue, + IsCyclic = cyclic ?? false, + NumbersToCache = numbersToCache ?? 1 + }.Serialize(); + + return propertyBuilder.CanSetAnnotation(GaussDBAnnotationNames.IdentityOptions, value); + } + + #endregion Identity options + + #region Array value conversion + + /// + /// Configures a GaussDB array conversion. + /// + [Obsolete( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", + error: true)] + public static PropertyBuilder HasPostgresArrayConversion( + this PropertyBuilder propertyBuilder, + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression) + => throw new NotSupportedException( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); + + /// + /// Configures a GaussDB array conversion. + /// + [Obsolete( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", + error: true)] + public static PropertyBuilder> HasPostgresArrayConversion( + this PropertyBuilder> propertyBuilder, + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression) + => throw new NotSupportedException( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); + + /// + /// Configures a GaussDB array conversion. + /// + [Obsolete( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", + error: true)] + public static PropertyBuilder HasPostgresArrayConversion( + this PropertyBuilder propertyBuilder, + ValueConverter elementValueConverter) + => throw new NotSupportedException( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); + + /// + /// Configures a GaussDB array conversion. + /// + [Obsolete( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", + error: true)] + public static PropertyBuilder> HasPostgresArrayConversion( + this PropertyBuilder> propertyBuilder, + ValueConverter elementValueConverter) + => throw new NotSupportedException( + "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); + + #endregion Array value conversion + + #region Generated tsvector column + + // Note: tsvector properties can be configured with a generic API through the entity type builder + + /// + /// Configures the property to be a full-text search tsvector column over the given properties. + /// + /// The builder for the property 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. + /// + /// + /// An array of property names to be included in the tsvector. + /// A builder to further configure the property. + public static PropertyBuilder IsGeneratedTsVectorColumn( + this PropertyBuilder propertyBuilder, + string config, + params string[] includedPropertyNames) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NotNull(config, nameof(config)); + Check.NotEmpty(includedPropertyNames, nameof(includedPropertyNames)); + + propertyBuilder.HasColumnType("tsvector"); + propertyBuilder.Metadata.SetTsVectorConfig(config); + propertyBuilder.Metadata.SetTsVectorProperties(includedPropertyNames); + + return propertyBuilder; + } + + /// + /// Configures the property to be a full-text search tsvector column over the given properties. + /// + /// The builder for the property 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. + /// + /// + /// An array of property names to be included in the tsvector. + /// A builder to further configure the property. + public static PropertyBuilder IsGeneratedTsVectorColumn( + this PropertyBuilder propertyBuilder, + string config, + params string[] includedPropertyNames) + => (PropertyBuilder)IsGeneratedTsVectorColumn((PropertyBuilder)propertyBuilder, config, includedPropertyNames); + + /// + /// Configures the property to be a full-text search tsvector column over the given properties. + /// + /// The builder for the property being configured. + /// + /// + /// The text search configuration for this generated tsvector property. + /// + /// + /// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information. + /// + /// + /// An array of property names to be included in the tsvector. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + public static IConventionPropertyBuilder? IsGeneratedTsVectorColumn( + this IConventionPropertyBuilder propertyBuilder, + string config, + IReadOnlyList includedPropertyNames, + bool fromDataAnnotation = false) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + if (propertyBuilder.CanSetIsGeneratedTsVectorColumn(config, includedPropertyNames, fromDataAnnotation)) + { + propertyBuilder.HasColumnType("tsvector"); + propertyBuilder.Metadata.SetTsVectorConfig(config, fromDataAnnotation); + propertyBuilder.Metadata.SetTsVectorProperties(includedPropertyNames, fromDataAnnotation); + + return propertyBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the property can be configured as a full-text search tsvector column. + /// + /// The builder for the property 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. + /// + /// + /// An array of property names to be included in the tsvector. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the property can be configured as a full-text search tsvector column. + public static bool CanSetIsGeneratedTsVectorColumn( + this IConventionPropertyBuilder propertyBuilder, + string? config, + IReadOnlyList? includedPropertyNames, + bool fromDataAnnotation = false) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + return (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + .Overrides(propertyBuilder.Metadata.GetTsVectorConfigConfigurationSource()) + && (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + .Overrides(propertyBuilder.Metadata.GetTsVectorPropertiesConfigurationSource()) + || config == propertyBuilder.Metadata.GetTsVectorConfig() + && StructuralComparisons.StructuralEqualityComparer.Equals( + includedPropertyNames, propertyBuilder.Metadata.GetTsVectorProperties()); + } + + #endregion Generated tsvector column + + #region Compression method + + /// + /// Sets the compression method for the column. + /// + /// This feature was introduced in GaussDB 14. + /// The builder for the property being configured. + /// The compression method. + /// A builder to further configure the property. + public static PropertyBuilder UseCompressionMethod( + this PropertyBuilder propertyBuilder, + string? compressionMethod) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NullButNotEmpty(compressionMethod, nameof(compressionMethod)); + + propertyBuilder.Metadata.SetCompressionMethod(compressionMethod); + + return propertyBuilder; + } + + /// + /// Sets the compression method for the column. + /// + /// This feature was introduced in GaussDB 14. + /// The builder for the property being configured. + /// The compression method. + /// A builder to further configure the property. + public static PropertyBuilder UseCompressionMethod( + this PropertyBuilder propertyBuilder, + string? compressionMethod) + => (PropertyBuilder)UseCompressionMethod((PropertyBuilder)propertyBuilder, compressionMethod); + + /// + /// Sets the compression method for the column. + /// + /// This feature was introduced in GaussDB 14. + /// The builder for the property being configured. + /// The compression method. + /// Indicates whether the configuration was specified using a data annotation. + /// A builder to further configure the property. + public static IConventionPropertyBuilder? UseCompressionMethod( + this IConventionPropertyBuilder propertyBuilder, + string? compressionMethod, + bool fromDataAnnotation = false) + { + if (propertyBuilder.CanSetCompressionMethod(compressionMethod, fromDataAnnotation)) + { + propertyBuilder.Metadata.SetCompressionMethod(compressionMethod, fromDataAnnotation); + + return propertyBuilder; + } + + return null; + } + + /// + /// Whether the compression method for the column can be set. + /// + /// This feature was introduced in GaussDB 14. + /// The builder for the property being configured. + /// The compression method. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the index can be configured with the method + public static bool CanSetCompressionMethod( + this IConventionPropertyBuilder propertyBuilder, + string? compressionMethod, + bool fromDataAnnotation = false) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + return propertyBuilder.CanSetAnnotation(GaussDBAnnotationNames.CompressionMethod, compressionMethod, fromDataAnnotation); + } + + #endregion Compression method +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBAggregateDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBAggregateDbFunctionsExtensions.cs new file mode 100644 index 0000000000..2b6ff3282a --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBAggregateDbFunctionsExtensions.cs @@ -0,0 +1,503 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods supporting aggregate function translation for GaussDB. +/// +public static class GaussDBAggregateDbFunctionsExtensions +{ + /// + /// Collects all the input values, including nulls, into a GaussDB array. + /// Corresponds to the GaussDB array_agg aggregate function. + /// + /// The instance. + /// The input values to be aggregated into an array. + /// GaussDB documentation for aggregate functions. + public static T[] ArrayAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ArrayAgg))); + + /// + /// Collects all the input values, including nulls, into a json array. Values are converted to JSON as per to_json or + /// to_jsonb. Corresponds to the GaussDB json_agg aggregate function. + /// + /// The instance. + /// The input values to be aggregated into a JSON array. + /// GaussDB documentation for aggregate functions. + public static T[] JsonAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonAgg))); + + /// + /// Collects all the input values, including nulls, into a jsonb array. Values are converted to JSON as per to_json or + /// to_jsonb. Corresponds to the GaussDB jsonb_agg aggregate function. + /// + /// The instance. + /// The input values to be aggregated into a JSON array. + /// GaussDB documentation for aggregate functions. + public static T[] JsonbAgg(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonbAgg))); + + /// + /// Computes the sum of the non-null input intervals. Corresponds to the GaussDB sum aggregate function. + /// + /// The instance. + /// The input values to be summed. + /// GaussDB documentation for aggregate functions. + public static TimeSpan? Sum(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Sum))); + + /// + /// Computes the average (arithmetic mean) of the non-null input intervals. Corresponds to the GaussDB avg aggregate function. + /// + /// The instance. + /// The input values to be computed into an average. + /// GaussDB documentation for aggregate functions. + public static TimeSpan? Average(this DbFunctions _, IEnumerable input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); + + // See additional range aggregate functions in GaussDBRangeDbfunctionsExtensions + + #region JsonObjectAgg + + /// + /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per + /// to_json. Values can be , but not keys. + /// Corresponds to the GaussDB json_object_agg aggregate function. + /// + /// The instance. + /// An enumerable of key-value pairs to be aggregated into a JSON object. + /// GaussDB documentation for aggregate functions. + public static string JsonObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonObjectAgg))); + + /// + /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per + /// to_json. Values can be , but not keys. + /// Corresponds to the GaussDB json_object_agg aggregate function. + /// + /// The instance. + /// An enumerable of key-value pairs to be aggregated into a JSON object. + /// GaussDB documentation for aggregate functions. + public static TReturn JsonObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonObjectAgg))); + + /// + /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per + /// to_jsonb. Values can be , but not keys. + /// Corresponds to the GaussDB jsonb_object_agg aggregate function. + /// + /// The instance. + /// An enumerable of key-value pairs to be aggregated into a JSON object. + /// GaussDB documentation for aggregate functions. + public static string JsonbObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonbObjectAgg))); + + /// + /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per + /// to_jsonb. Values can be , but not keys. + /// Corresponds to the GaussDB jsonb_object_agg aggregate function. + /// + /// The instance. + /// An enumerable of key-value pairs to be aggregated into a JSON object. + /// GaussDB documentation for aggregate functions. + public static TReturn JsonbObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonbObjectAgg))); + + #endregion JsonObjectAgg + + #region Sample standard deviation + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_samp function. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + #endregion Sample standard deviation + + #region Population standard deviation + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to the GaussDB stddev_pop function. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + #endregion Population standard deviation + + #region Sample variance + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to the GaussDB var_samp function. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + #endregion Sample variance + + #region Population variance + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to the GaussDB var_pop function. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + #endregion Population variance + + #region Other statistics functions + + /// + /// Computes the correlation coefficient. Corresponds to the GaussDB corr function. + /// + /// The instance. + /// The values. + public static double? Correlation(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Correlation))); + + /// + /// Computes the population covariance. Corresponds to the GaussDB covar_pop function. + /// + /// The instance. + /// The values. + public static double? CovariancePopulation(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CovariancePopulation))); + + /// + /// Computes the sample covariance. Corresponds to the GaussDB covar_samp function. + /// + /// The instance. + /// The values. + public static double? CovarianceSample(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CovarianceSample))); + + /// + /// Computes the average of the independent variable, sum(X)/N. + /// Corresponds to the GaussDB regr_avgx function. + /// + /// The instance. + /// The values. + public static double? RegrAverageX(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrAverageX))); + + /// + /// Computes the average of the dependent variable, sum(Y)/N. + /// Corresponds to the GaussDB regr_avgy function. + /// + /// The instance. + /// The values. + public static double? RegrAverageY(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrAverageY))); + + /// + /// Computes the number of rows in which both inputs are non-null. + /// Corresponds to the GaussDB regr_count function. + /// + /// The instance. + /// The values. + public static long? RegrCount(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrCount))); + + /// + /// Computes the y-intercept of the least-squares-fit linear equation determined by the (X, Y) pairs. + /// Corresponds to the GaussDB regr_intercept function. + /// + /// The instance. + /// The values. + public static double? RegrIntercept(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrIntercept))); + + /// + /// Computes the square of the correlation coefficient. + /// Corresponds to the GaussDB regr_r2 function. + /// + /// The instance. + /// The values. + public static double? RegrR2(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrR2))); + + /// + /// Computes the slope of the least-squares-fit linear equation determined by the (X, Y) pairs. + /// Corresponds to the GaussDB regr_slope function. + /// + /// The instance. + /// The values. + public static double? RegrSlope(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrSlope))); + + /// + /// Computes the “sum of squares” of the independent variable, sum(X^2) - sum(X)^2/N. + /// Corresponds to the GaussDB regr_sxx function. + /// + /// The instance. + /// The values. + public static double? RegrSXX(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrSXX))); + + /// + /// Computes the “sum of products” of independent times dependent variables, sum(X*Y) - sum(X) * sum(Y)/N. + /// Corresponds to the GaussDB regr_sxy function. + /// + /// The instance. + /// The values. + public static double? RegrSXY(this DbFunctions _, IEnumerable<(double, double)> values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrSXY))); + + #endregion Other statistics functions +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBDbFunctionsExtensions.cs new file mode 100644 index 0000000000..109e9f80a2 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBDbFunctionsExtensions.cs @@ -0,0 +1,164 @@ +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides GaussDB-specific extension methods on . +/// +public static class GaussDBDbFunctionsExtensions +{ + // ReSharper disable once InconsistentNaming + /// + /// An implementation of the GaussDB ILIKE operation, which is an insensitive LIKE. + /// + /// The instance. + /// The string that is to be matched. + /// The pattern which may involve wildcards %,_,[,],^. + /// if there is a match. + public static bool ILike(this DbFunctions _, string matchExpression, string pattern) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ILike))); + + // ReSharper disable once InconsistentNaming + /// + /// An implementation of the GaussDB ILIKE operation, which is an insensitive LIKE. + /// + /// The instance. + /// The string that is to be matched. + /// The pattern which may involve wildcards %,_,[,],^. + /// + /// The escape character (as a single character string) to use in front of %,_,[,],^ + /// if they are not used as wildcards. + /// + /// if there is a match. + public static bool ILike(this DbFunctions _, string matchExpression, string pattern, string escapeCharacter) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ILike))); + + /// + /// Splits at occurrences of delimiter and forms the resulting fields into a text array. + /// + /// The instance. + /// The string to be split. + /// + /// If null, each character in the string will become a separate element in the array. + /// If an empty string, the string is treated as a single field. + /// + /// + public static string[] StringToArray(this DbFunctions _, string value, string delimiter) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StringToArray))); + + /// + /// Splits at occurrences of delimiter and forms the resulting fields into a text array. + /// + /// The instance. + /// The string to be split. + /// + /// If null, each character in the string will become a separate element in the array. + /// If an empty string, the string is treated as a single field. + /// + /// Fields matching this value string are replaced by null. + public static string[] StringToArray(this DbFunctions _, string value, string delimiter, string nullString) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StringToArray))); + + /// + /// Reverses a string by calling GaussDB reverse(). + /// + /// The instance. + /// The string that is to be reversed. + /// The reversed string. + public static string Reverse(this DbFunctions _, string value) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Reverse))); + + /// + /// Returns whether the row value represented by is greater than the row value represented by + /// . + /// + /// + /// For more information on row value comparisons, see + /// + /// the GaussDB documentation. + /// + /// + public static bool GreaterThan(this DbFunctions _, ITuple a, ITuple b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan))); + + /// + /// Returns whether the row value represented by is less than the row value represented by . + /// + /// + /// For more information on row value comparisons, see + /// + /// the GaussDB documentation. + /// + /// + public static bool LessThan(this DbFunctions _, ITuple a, ITuple b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan))); + + /// + /// Returns whether the row value represented by is greater than or equal to the row value represented by + /// . + /// + /// + /// For more information on row value comparisons, see + /// + /// the GaussDB documentation. + /// + /// + public static bool GreaterThanOrEqual(this DbFunctions _, ITuple a, ITuple b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual))); + + /// + /// Returns whether the row value represented by is less than or equal to the row value represented by + /// . + /// + /// + /// For more information on row value comparisons, see + /// + /// the GaussDB documentation. + /// + /// + public static bool LessThanOrEqual(this DbFunctions _, ITuple a, ITuple b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual))); + + /// + /// Returns the distance between two dates as a number of days, particularly suitable for sorting where the appropriate index is + /// defined. + /// + /// + /// This requires the btree_gist built-in GaussDB extension, see + /// . + /// + public static int Distance(this DbFunctions _, DateOnly a, DateOnly b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); + + /// + /// Returns the distance between two timestamps as a GaussDB interval, particularly suitable for sorting where the appropriate + /// index is defined. + /// + /// + /// This requires the btree_gist built-in GaussDB extension, see + /// . + /// + 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.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFullTextSearchDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFullTextSearchDbFunctionsExtensions.cs new file mode 100644 index 0000000000..e129797866 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFullTextSearchDbFunctionsExtensions.cs @@ -0,0 +1,151 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.GaussDBTypes; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. +/// The methods on this class are accessed via . +/// +/// +/// See Database functions. +/// +[SuppressMessage("ReSharper", "UnusedParameter.Global")] +public static class GaussDBFullTextSearchDbFunctionsExtensions +{ + /// + /// Convert to a tsvector. + /// + /// + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static GaussDBTsVector ArrayToTsVector(this DbFunctions _, string[] lexemes) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ArrayToTsVector))); + + /// + /// Reduce to tsvector. + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-DOCUMENTS + /// + public static GaussDBTsVector ToTsVector(this DbFunctions _, string document) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsVector))); + + /// + /// Reduce to tsvector using the text search configuration specified + /// by . + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-DOCUMENTS + /// + public static GaussDBTsVector ToTsVector(this DbFunctions _, [NotParameterized] string config, string document) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsVector))); + + /// + /// Produce tsquery from ignoring punctuation. + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery PlainToTsQuery(this DbFunctions _, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PlainToTsQuery))); + + /// + /// Produce tsquery from ignoring punctuation and using the text search + /// configuration specified by . + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery PlainToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PlainToTsQuery))); + + /// + /// Produce tsquery that searches for a phrase from ignoring punctuation. + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery PhraseToTsQuery(this DbFunctions _, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PhraseToTsQuery))); + + /// + /// Produce tsquery that searches for a phrase from ignoring punctuation + /// and using the text search configuration specified by . + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery PhraseToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PhraseToTsQuery))); + + /// + /// Normalize words in and convert to tsquery. If your input + /// contains punctuation that should not be treated as text search operators, use + /// instead. + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery ToTsQuery(this DbFunctions _, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsQuery))); + + /// + /// Normalize words in and convert to tsquery using the text search + /// configuration specified by . If your input contains punctuation + /// that should not be treated as text search operators, use + /// instead. + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery ToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsQuery))); + + /// + /// Convert tsquery using the simplified websearch syntax. + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery WebSearchToTsQuery(this DbFunctions _, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(WebSearchToTsQuery))); + + /// + /// Convert tsquery using the simplified websearch syntax and the text + /// search configuration specified by . + /// + /// + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES + /// + public static GaussDBTsQuery WebSearchToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(WebSearchToTsQuery))); + + /// + /// Returns a new string that removes diacritics from characters in the given . + /// + /// The instance. + /// A specific text search dictionary. + /// The text to remove the diacritics. + /// + /// The method call is translated to unaccent(regdictionary, text). + /// See https://www.postgresql.org/docs/current/unaccent.html. + /// + /// A string without diacritics. + public static string Unaccent(this DbFunctions _, string regDictionary, string text) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Unaccent))); + + /// + /// Returns a new string that removes diacritics from characters in the given . + /// + /// The instance. + /// The text to remove the diacritics. + /// + /// The method call is translated to unaccent(text). + /// See https://www.postgresql.org/docs/current/unaccent.html. + /// + /// A string without diacritics. + public static string Unaccent(this DbFunctions _, string text) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Unaccent))); +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFullTextSearchLinqExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFullTextSearchLinqExtensions.cs new file mode 100644 index 0000000000..a7b5f5e2dc --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFullTextSearchLinqExtensions.cs @@ -0,0 +1,285 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.GaussDBTypes; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides EF Core extension methods for GaussDB full-text search types. +/// +[SuppressMessage("ReSharper", "UnusedParameter.Global")] +public static class GaussDBFullTextSearchLinqExtensions +{ + /// + /// AND tsquerys together. Generates the "&&" operator. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery And(this GaussDBTsQuery query1, GaussDBTsQuery query2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(And))); + + /// + /// OR tsquerys together. Generates the "||" operator. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery Or(this GaussDBTsQuery query1, GaussDBTsQuery query2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(Or))); + + /// + /// Negate a tsquery. Generates the "!!" operator. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery ToNegative(this GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(ToNegative))); + + /// + /// Returns whether contains . + /// Generates the "@>" operator. + /// http://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static bool Contains(this GaussDBTsQuery query1, GaussDBTsQuery query2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(Contains))); + + /// + /// Returns whether is contained within . + /// Generates the "<@" operator. + /// http://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static bool IsContainedIn(this GaussDBTsQuery query1, GaussDBTsQuery query2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(IsContainedIn))); + + /// + /// Returns the number of lexemes plus operators in . + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static int GetNodeCount(this GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(GetNodeCount))); + + /// + /// Get the indexable part of . + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static string GetQueryTree(this GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(GetQueryTree))); + + /// + /// Returns a string suitable for display containing a query match. + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE + /// + public static string GetResultHeadline(this GaussDBTsQuery query, string document) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(GetResultHeadline))); + + /// + /// Returns a string suitable for display containing a query match. + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE + /// + public static string GetResultHeadline(this GaussDBTsQuery query, string document, string options) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(GetResultHeadline))); + + /// + /// Returns a string suitable for display containing a query match using the text + /// search configuration specified by . + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE + /// + public static string GetResultHeadline(this GaussDBTsQuery query, string config, string document, string options) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(GetResultHeadline))); + + /// + /// Searches for occurrences of , and replaces + /// each occurrence with a . All parameters are of type tsquery. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery Rewrite(this GaussDBTsQuery query, GaussDBTsQuery target, GaussDBTsQuery substitute) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(Rewrite))); + + /// + /// For each row of the SQL result, occurrences of the first column value (the target) + /// are replaced by the second column value (the substitute) within the current value. + /// The must yield two columns of tsquery type. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery Rewrite(this GaussDBTsQuery query, string select) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(Rewrite))); + + /// + /// Returns a tsquery that searches for a match to followed by a match + /// to . + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery ToPhrase(this GaussDBTsQuery query1, GaussDBTsQuery query2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(ToPhrase))); + + /// + /// Returns a tsquery that searches for a match to followed by a match + /// to at a distance of lexemes using + /// the <N> tsquery operator + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY + /// + public static GaussDBTsQuery ToPhrase(this GaussDBTsQuery query1, GaussDBTsQuery query2, int distance) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsQuery) + "." + nameof(ToPhrase))); + + /// + /// This method generates the "@@" match operator. The parameter is + /// assumed to be a plain search query and will be converted to a tsquery using plainto_tsquery. + /// http://www.postgresql.org/docs/current/static/textsearch-intro.html#TEXTSEARCH-MATCHING + /// + public static bool Matches(this GaussDBTsVector vector, string query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Matches))); + + /// + /// This method generates the "@@" match operator. + /// http://www.postgresql.org/docs/current/static/textsearch-intro.html#TEXTSEARCH-MATCHING + /// + public static bool Matches(this GaussDBTsVector vector, GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Matches))); + + /// + /// Returns a vector which combines the lexemes and positional information of + /// and using the || tsvector operator. Positions and weight labels are retained + /// during the concatenation. + /// https://www.postgresql.org/docs/10/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static GaussDBTsVector Concat(this GaussDBTsVector vector1, GaussDBTsVector vector2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Concat))); + + /// + /// Assign weight to each element of and return a new + /// weighted tsvector. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static GaussDBTsVector SetWeight(this GaussDBTsVector vector, GaussDBTsVector.Lexeme.Weight weight) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(SetWeight))); + + /// + /// Assign weight to elements of that are in and + /// return a new weighted tsvector. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static GaussDBTsVector SetWeight(this GaussDBTsVector vector, GaussDBTsVector.Lexeme.Weight weight, string[] lexemes) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(SetWeight))); + + /// + /// Assign weight to each element of and return a new + /// weighted tsvector. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static GaussDBTsVector SetWeight(this GaussDBTsVector vector, char weight) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(SetWeight))); + + /// + /// Assign weight to elements of that are in and + /// return a new weighted tsvector. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static GaussDBTsVector SetWeight(this GaussDBTsVector vector, char weight, string[] lexemes) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(SetWeight))); + + /// + /// Return a new vector with removed from + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static GaussDBTsVector Delete(this GaussDBTsVector vector, string lexeme) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Delete))); + + /// + /// Return a new vector with removed from + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static GaussDBTsVector Delete(this GaussDBTsVector vector, string[] lexemes) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Delete))); + + /// + /// Returns a new vector with only lexemes having weights specified in . + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static GaussDBTsVector Filter(this GaussDBTsVector vector, char[] weights) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Filter))); + + /// + /// Returns the number of lexemes in . + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static int GetLength(this GaussDBTsVector vector) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(GetLength))); + + /// + /// Removes weights and positions from and returns + /// a new stripped tsvector. + /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static GaussDBTsVector ToStripped(this GaussDBTsVector vector) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(ToStripped))); + + /// + /// Calculates the rank of for . + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float Rank(this GaussDBTsVector vector, GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Rank))); + + /// + /// Calculates the rank of for while normalizing + /// the result according to the behaviors specified by . + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float Rank(this GaussDBTsVector vector, GaussDBTsQuery query, GaussDBTsRankingNormalization normalization) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Rank))); + + /// + /// Calculates the rank of for with custom + /// weighting for word instances depending on their labels (D, C, B or A). + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float Rank(this GaussDBTsVector vector, float[] weights, GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Rank))); + + /// + /// Calculates the rank of for while normalizing + /// the result according to the behaviors specified by + /// and using custom weighting for word instances depending on their labels (D, C, B or A). + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float Rank( + this GaussDBTsVector vector, + float[] weights, + GaussDBTsQuery query, + GaussDBTsRankingNormalization normalization) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(Rank))); + + /// + /// Calculates the rank of for using the cover + /// density method. + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float RankCoverDensity(this GaussDBTsVector vector, GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(RankCoverDensity))); + + /// + /// Calculates the rank of for using the cover + /// density method while normalizing the result according to the behaviors specified by + /// . + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float RankCoverDensity(this GaussDBTsVector vector, GaussDBTsQuery query, GaussDBTsRankingNormalization normalization) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(RankCoverDensity))); + + /// + /// Calculates the rank of for using the cover + /// density method with custom weighting for word instances depending on their labels (D, C, B or A). + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float RankCoverDensity(this GaussDBTsVector vector, float[] weights, GaussDBTsQuery query) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(RankCoverDensity))); + + /// + /// Calculates the rank of for using the cover density + /// method while normalizing the result according to the behaviors specified by + /// and using custom weighting for word instances depending on their labels (D, C, B or A). + /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING + /// + public static float RankCoverDensity( + this GaussDBTsVector vector, + float[] weights, + GaussDBTsQuery query, + GaussDBTsRankingNormalization normalization) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GaussDBTsVector) + "." + nameof(RankCoverDensity))); +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFuzzyStringMatchDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFuzzyStringMatchDbFunctionsExtensions.cs new file mode 100644 index 0000000000..8e50babe4a --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBFuzzyStringMatchDbFunctionsExtensions.cs @@ -0,0 +1,126 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. +/// The methods on this class are accessed via . +/// +/// +/// See Database functions. +/// +public static class GaussDBFuzzyStringMatchDbFunctionsExtensions +{ + /// + /// The soundex function converts a string to its Soundex code. + /// + /// + /// The method call is translated to soundex(text). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchSoundex(this DbFunctions _, string text) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchSoundex))); + + /// + /// The difference function converts two strings to their Soundex codes and + /// then returns the number of matching code positions. Since Soundex codes + /// have four characters, the result ranges from zero to four, with zero being + /// no match and four being an exact match. + /// + /// + /// The method call is translated to difference(source, target). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchDifference(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDifference))); + + /// + /// Returns the Levenshtein distance between two strings. + /// + /// + /// The method call is translated to levenshtein(source, target). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshtein(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshtein))); + + /// + /// Returns the Levenshtein distance between two strings. + /// + /// + /// The method call is translated to levenshtein(source, target, insertionCost, deletionCost, substitutionCost). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshtein( + this DbFunctions _, + string source, + string target, + int insertionCost, + int deletionCost, + int substitutionCost) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshtein))); + + /// + /// levenshtein_less_equal is an accelerated version of the Levenshtein function for use when only small distances are of interest. + /// If the actual distance is less than or equal to maximum distance, then levenshtein_less_equal returns the correct distance; + /// otherwise it returns some value greater than maximum distance. If maximum distance is negative then the behavior is the same as + /// levenshtein. + /// + /// + /// The method call is translated to levenshtein_less_equal(source, target, maximumDistance). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshteinLessEqual(this DbFunctions _, string source, string target, int maximumDistance) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshteinLessEqual))); + + /// + /// levenshtein_less_equal is an accelerated version of the Levenshtein function for use when only small distances are of interest. + /// If the actual distance is less than or equal to maximum distance, then levenshtein_less_equal returns the correct distance; + /// otherwise it returns some value greater than maximum distance. If maximum distance is negative then the behavior is the same as + /// levenshtein. + /// + /// + /// The method call is translated to + /// levenshtein_less_equal(source, target, insertionCost, deletionCost, substitutionCost, maximumDistance). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshteinLessEqual( + this DbFunctions _, + string source, + string target, + int insertionCost, + int deletionCost, + int substitutionCost, + int maximumDistance) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshteinLessEqual))); + + /// + /// The metaphone function converts a string to its Metaphone code. + /// + /// + /// The method call is translated to metaphone(text, maximumOutputLength). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchMetaphone(this DbFunctions _, string text, int maximumOutputLength) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchMetaphone))); + + /// + /// The dmetaphone function converts a string to its primary Double Metaphone code. + /// + /// + /// The method call is translated to dmetaphone(text). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchDoubleMetaphone(this DbFunctions _, string text) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDoubleMetaphone))); + + /// + /// The dmetaphone_alt function converts a string to its alternate Double Metaphone code. + /// + /// + /// The method call is translated to dmetaphone_alt(text). + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchDoubleMetaphoneAlt(this DbFunctions _, string text) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDoubleMetaphoneAlt))); +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBJsonDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBJsonDbFunctionsExtensions.cs new file mode 100644 index 0000000000..9e6f2e3712 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBJsonDbFunctionsExtensions.cs @@ -0,0 +1,110 @@ +using System.Text.Json; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides methods for supporting translation to GaussDB JSON operators and functions. +/// +public static class GaussDBJsonDbFunctionsExtensions +{ + /// + /// Checks if contains as top-level entries. + /// + /// DbFunctions instance + /// + /// A JSON column or value. Can be a , a string property mapped to JSON, + /// or a user POCO mapped to JSON. + /// + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// + /// This operation is only supported with GaussDB jsonb, not json. + /// See https://www.postgresql.org/docs/current/functions-json.html. + /// + public static bool JsonContains( + this DbFunctions _, + object json, + object contained) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonContains))); + + /// + /// Checks if is contained in as top-level entries. + /// + /// DbFunctions instance + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// + /// This operation is only supported with GaussDB jsonb, not json. + /// See https://www.postgresql.org/docs/current/functions-json.html. + /// + public static bool JsonContained( + this DbFunctions _, + object contained, + object json) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonContained))); + + /// + /// Checks if exists as a top-level key within . + /// + /// DbFunctions instance + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// A key to be checked inside . + /// + /// This operation is only supported with GaussDB jsonb, not json. + /// See https://www.postgresql.org/docs/current/functions-json.html. + /// + public static bool JsonExists(this DbFunctions _, object json, string key) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists))); + + /// + /// Checks if any of the given exist as top-level keys within . + /// + /// DbFunctions instance + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// A set of keys to be checked inside . + /// + /// This operation is only supported with GaussDB jsonb, not json. + /// See https://www.postgresql.org/docs/current/functions-json.html. + /// + public static bool JsonExistAny(this DbFunctions _, object json, params string[] keys) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExistAny))); + + /// + /// Checks if all of the given exist as top-level keys within . + /// + /// DbFunctions instance + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// A set of keys to be checked inside . + /// + /// This operation is only supported with GaussDB jsonb, not json. + /// See https://www.postgresql.org/docs/current/functions-json.html. + /// + public static bool JsonExistAll(this DbFunctions _, object json, params string[] keys) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExistAll))); + + /// + /// Returns the type of the outermost JSON value as a text string. + /// Possible types are object, array, string, number, boolean, and null. + /// + /// DbFunctions instance + /// + /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. + /// + /// + /// See https://www.postgresql.org/docs/current/functions-json.html. + /// + public static string JsonTypeof(this DbFunctions _, object json) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonTypeof))); +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBMultirangeDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBMultirangeDbFunctionsExtensions.cs new file mode 100644 index 0000000000..0d540ee99b --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBMultirangeDbFunctionsExtensions.cs @@ -0,0 +1,770 @@ +// ReSharper disable once CheckNamespace + +using HuaweiCloud.GaussDBTypes; + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods for multiranges supporting GaussDB translation. +/// +public static class GaussDBMultirangeDbFunctionsExtensions +{ + #region Contains + + /// + /// Determines whether a multirange contains a specified value. + /// + /// The multirange in which to locate the value. + /// The value to locate in the range. + /// + /// true + /// if the multirange contains the specified value; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Contains(this GaussDBRange[] multirange, T value) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a multirange contains a specified value. + /// + /// The multirange in which to locate the value. + /// The value to locate in the range. + /// + /// true + /// if the multirange contains the specified value; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Contains(this List> multirange, T value) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a multirange contains a specified multirange. + /// + /// The multirange in which to locate the specified multirange. + /// The specified multirange to locate in the multirange. + /// + /// true + /// if the multirange contains the specified multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool Contains(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a multirange contains a specified multirange. + /// + /// The multirange in which to locate the specified multirange. + /// The specified multirange to locate in the multirange. + /// + /// true + /// if the multirange contains the specified multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static bool Contains(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a multirange contains a specified range. + /// + /// The multirange in which to locate the specified range. + /// The specified range to locate in the multirange. + /// + /// true + /// if the multirange contains the specified range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static bool Contains(this GaussDBRange[] multirange1, GaussDBRange multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a multirange contains a specified range. + /// + /// The multirange in which to locate the specified range. + /// The specified range to locate in the multirange. + /// + /// true + /// if the multirange contains the specified range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool Contains(this List> multirange1, GaussDBRange multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + #endregion Contains + + #region ContainedBy + + /// + /// Determines whether a multirange is contained by a specified multirange. + /// + /// The specified multirange to locate in the multirange. + /// The multirange in which to locate the specified multirange. + /// + /// true + /// if the multirange contains the specified multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool ContainedBy(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Determines whether a multirange is contained by a specified multirange. + /// + /// The specified multirange to locate in the multirange. + /// The multirange in which to locate the specified multirange. + /// + /// true + /// if the multirange contains the specified multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool ContainedBy(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Determines whether a range is contained by a specified multirange. + /// + /// The specified range to locate in the multirange. + /// The multirange in which to locate the specified range. + /// + /// true + /// if the multirange contains the specified range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool ContainedBy(this GaussDBRange range, GaussDBRange[] multirange) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Determines whether a range is contained by a specified multirange. + /// + /// The specified range to locate in the multirange. + /// The multirange in which to locate the specified range. + /// + /// true + /// if the multirange contains the specified range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static bool ContainedBy(this GaussDBRange range, List> multirange) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + #endregion ContainedBy + + #region Overlaps + + /// + /// Determines whether a multirange overlaps another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the multiranges overlap (share points in common); otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool Overlaps(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); + + /// + /// Determines whether a multirange overlaps another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the multiranges overlap (share points in common); otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool Overlaps(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); + + /// + /// Determines whether a multirange overlaps another range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange and range overlap (share points in common); otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static bool Overlaps(this GaussDBRange[] multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); + + /// + /// Determines whether a multirange overlaps another range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange and range overlap (share points in common); otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool Overlaps(this List> multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); + + #endregion Overlaps + + #region IsStrictlyLeftOf + + /// + /// Determines whether a multirange is strictly to the left of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange is strictly to the left of the second multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static bool IsStrictlyLeftOf(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); + + /// + /// Determines whether a multirange is strictly to the left of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange is strictly to the left of the second multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part + /// of an EF Core LINQ query. + /// + public static bool IsStrictlyLeftOf(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); + + /// + /// Determines whether a multirange is strictly to the left of a range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange is strictly to the left of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static bool IsStrictlyLeftOf(this GaussDBRange[] multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); + + /// + /// Determines whether a multirange is strictly to the left of a range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange is strictly to the left of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool IsStrictlyLeftOf(this List> multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); + + #endregion IsStrictlyLeftOf + + #region IsStrictlyRightOf + + /// + /// Determines whether a multirange is strictly to the right of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange is strictly to the right of the second multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool IsStrictlyRightOf(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); + + /// + /// Determines whether a multirange is strictly to the right of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange is strictly to the right of the second multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part + /// of an EF Core LINQ query. + /// + public static bool IsStrictlyRightOf(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); + + /// + /// Determines whether a multirange is strictly to the right of a range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange is strictly to the right of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static bool IsStrictlyRightOf(this GaussDBRange[] multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); + + /// + /// Determines whether a multirange is strictly to the right of a range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange is strictly to the right of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool IsStrictlyRightOf(this List> multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); + + #endregion IsStrictlyRightOf + + #region DoesNotExtendLeftOf + + /// + /// Determines whether a multirange does not extend to the left of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange does not extend to the left of the multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool DoesNotExtendLeftOf(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); + + /// + /// Determines whether a multirange does not extend to the left of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange does not extend to the left of the multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as + /// part of an EF Core LINQ query. + /// + public static bool DoesNotExtendLeftOf(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); + + /// + /// Determines whether a multirange does not extend to the left of a range. + /// + /// The multirange. + /// The multirange. + /// + /// true + /// if the multirange does not extend to the left of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool DoesNotExtendLeftOf(this GaussDBRange[] multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); + + /// + /// Determines whether a multirange does not extend to the left of a range. + /// + /// The multirange. + /// The multirange. + /// + /// true + /// if the multirange does not extend to the left of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of + /// an EF Core LINQ query. + /// + public static bool DoesNotExtendLeftOf(this List> multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); + + #endregion DoesNotExtendLeftOf + + #region DoesNotExtendRightOf + + /// + /// Determines whether a multirange does not extend to the right of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange does not extend to the right of the multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of + /// an EF Core LINQ query. + /// + public static bool DoesNotExtendRightOf(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); + + /// + /// Determines whether a multirange does not extend to the right of another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the first multirange does not extend to the right of the multirange; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as + /// part of an EF Core LINQ query. + /// + public static bool DoesNotExtendRightOf(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); + + /// + /// Determines whether a multirange does not extend to the right of a range. + /// + /// The multirange. + /// The multirange. + /// + /// true + /// if the multirange does not extend to the right of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static bool DoesNotExtendRightOf(this GaussDBRange[] multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); + + /// + /// Determines whether a multirange does not extend to the right of a range. + /// + /// The multirange. + /// The multirange. + /// + /// true + /// if the multirange does not extend to the right of the range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of + /// an EF Core LINQ query. + /// + public static bool DoesNotExtendRightOf(this List> multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); + + #endregion DoesNotExtendRightOf + + #region IsAdjacentTo + + /// + /// Determines whether a multirange is adjacent to another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the multiranges are adjacent; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool IsAdjacentTo(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); + + /// + /// Determines whether a multirange is adjacent to another multirange. + /// + /// The first multirange. + /// The second multirange. + /// + /// true + /// if the multiranges are adjacent; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of + /// an EF Core LINQ query. + /// + public static bool IsAdjacentTo(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); + + /// + /// Determines whether a multirange is adjacent to a range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange and range are adjacent; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static bool IsAdjacentTo(this GaussDBRange[] multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); + + /// + /// Determines whether a multirange is adjacent to a range. + /// + /// The multirange. + /// The range. + /// + /// true + /// if the multirange and range are adjacent; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static bool IsAdjacentTo(this List> multirange, GaussDBRange range) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); + + #endregion IsAdjacentTo + + #region Union + + /// + /// Returns the set union, which means unique elements that appear in either of two multiranges. + /// + /// The first multirange. + /// The second multirange. + /// A multirange containing the unique elements that appear in either multirange. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static GaussDBRange[] Union(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); + + /// + /// Returns the set union, which means unique elements that appear in either of two multiranges. + /// + /// The first multirange. + /// The second multirange. + /// A multirange containing the unique elements that appear in either multirange. + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static List> Union(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); + + #endregion Union + + #region Intersect + + /// + /// Returns the set intersection, which means elements that appear in each of two multiranges. + /// + /// The first multirange. + /// The second multirange. + /// A multirange containing the elements that appear in both ranges. + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static GaussDBRange[] Intersect(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); + + /// + /// Returns the set intersection, which means elements that appear in each of two multiranges. + /// + /// The first multirange. + /// The second multirange. + /// A multirange containing the elements that appear in both ranges. + /// + /// is only intended for use via SQL translation as part of an + /// EF Core LINQ query. + /// + public static List> Intersect(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); + + #endregion Intersect + + #region Except + + /// + /// Returns the set difference, which means the elements of one multirange that do not appear in a second multirange. + /// + /// The first multirange. + /// The second multirange. + /// A multirange containing the elements that appear in the first range, but not the second range. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static GaussDBRange[] Except(this GaussDBRange[] multirange1, GaussDBRange[] multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Except))); + + /// + /// Returns the set difference, which means the elements of one multirange that do not appear in a second multirange. + /// + /// The first multirange. + /// The second multirange. + /// A multirange containing the elements that appear in the first range, but not the second range. + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static List> Except(this List> multirange1, List> multirange2) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Except))); + + #endregion Except + + #region Merge + + /// + /// Computes the smallest range that includes the entire multirange. + /// + /// The multirange. + /// The smallest range that includes the entire multirange. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBRange Merge(this GaussDBRange[] multirange) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); + + /// + /// Computes the smallest range that includes the entire multirange. + /// + /// The multirange. + /// The smallest range that includes the entire multirange. + /// + /// is only intended for use via SQL translation as part of an EF + /// Core LINQ query. + /// + public static GaussDBRange Merge(this List> multirange) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); + + #endregion Merge +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBNetworkDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBNetworkDbFunctionsExtensions.cs new file mode 100644 index 0000000000..689083e7bc --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBNetworkDbFunctionsExtensions.cs @@ -0,0 +1,1136 @@ +using System.Net; +using System.Net.NetworkInformation; +using HuaweiCloud.GaussDBTypes; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods supporting operator translation for GaussDB network types. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-net.html +/// +public static class GaussDBNetworkDbFunctionsExtensions +{ + #region RelationalOperators + + /// + /// Determines whether an is less than another . + /// + /// The instance. + /// The left-hand inet. + /// The right-hand inet. + /// + /// True if the is less than the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool LessThan(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan))); + + /// + /// Determines whether an is less than another . + /// + /// The instance. + /// The left-hand macaddr. + /// The right-hand macaddr. + /// + /// True if the is less than the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool LessThan(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan))); + + /// + /// Determines whether an is less than or equal to another . + /// + /// The instance. + /// The left-hand inet. + /// The right-hand inet. + /// + /// True if the is less than or equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool LessThanOrEqual(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual))); + + /// + /// Determines whether an is less than or equal to another . + /// + /// The instance. + /// The left-hand macaddr. + /// The right-hand macaddr. + /// + /// True if the is less than or equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool LessThanOrEqual(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual))); + + /// + /// Determines whether an is greater than or equal to another . + /// + /// The instance. + /// The left-hand inet. + /// The right-hand inet. + /// + /// True if the is greater than or equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool GreaterThanOrEqual(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual))); + + /// + /// Determines whether an is greater than or equal to another . + /// + /// The instance. + /// The left-hand macaddr. + /// The right-hand macaddr. + /// + /// True if the is greater than or equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool GreaterThanOrEqual(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual))); + + /// + /// Determines whether an is greater than another . + /// + /// The instance. + /// The left-hand inet. + /// The right-hand inet. + /// + /// True if the is greater than the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool GreaterThan(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan))); + + /// + /// Determines whether an is greater than another . + /// + /// The instance. + /// The left-hand macaddr. + /// The right-hand macaddr. + /// + /// True if the is greater than the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool GreaterThan(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan))); + + #endregion + + #region ContainmentOperators + + /// + /// Determines whether an is contained within another . + /// + /// The instance. + /// The inet to locate. + /// The inet to search. + /// + /// True if the is contained within the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool ContainedBy(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Determines whether an is contained within or equal to another . + /// + /// The instance. + /// The inet to locate. + /// The inet to search. + /// + /// True if the is contained within or equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool ContainedByOrEqual(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedByOrEqual))); + + /// + /// Determines whether an contains another . + /// + /// The instance. + /// The IP address to search. + /// The IP address to locate. + /// + /// True if the contains the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Contains(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether an contains or is equal to another . + /// + /// The instance. + /// The IP address to search. + /// The IP address to locate. + /// + /// True if the contains or is equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool ContainsOrEqual(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsOrEqual))); + + /// + /// Determines whether an contains or is contained by another . + /// + /// The instance. + /// The IP address to search. + /// The IP address to locate. + /// + /// True if the contains or is contained by the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool ContainsOrContainedBy(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsOrContainedBy))); + + #endregion + + #region BitwiseOperators + + /// + /// Computes the bitwise NOT operation on an . + /// + /// The instance. + /// The inet to negate. + /// + /// The result of the bitwise NOT operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet BitwiseNot(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseNot))); + + /// + /// Computes the bitwise NOT operation on an . + /// + /// The instance. + /// The macaddr to negate. + /// + /// The result of the bitwise NOT operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static PhysicalAddress BitwiseNot(this DbFunctions _, PhysicalAddress macaddr) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseNot))); + + /// + /// Computes the bitwise AND of two instances. + /// + /// The instance. + /// The left-hand inet. + /// The right-hand inet. + /// + /// The result of the bitwise AND operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet BitwiseAnd(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseAnd))); + + /// + /// Computes the bitwise AND of two instances. + /// + /// The instance. + /// The left-hand macaddr. + /// The right-hand macaddr. + /// + /// The result of the bitwise AND operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static PhysicalAddress BitwiseAnd(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseAnd))); + + /// + /// Computes the bitwise OR of two instances. + /// + /// The instance. + /// The left-hand inet. + /// The right-hand inet. + /// + /// The result of the bitwise OR operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet BitwiseOr(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseOr))); + + /// + /// Computes the bitwise OR of two instances. + /// + /// The instance. + /// The left-hand macaddr. + /// The right-hand macaddr. + /// + /// The result of the bitwise OR operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static PhysicalAddress BitwiseOr(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseOr))); + + #endregion + + #region ArithmeticOperators + + /// + /// Adds the to the . + /// + /// The instance. + /// The inet. + /// The value to add. + /// + /// The augmented by the . + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet Add(this DbFunctions _, GaussDBInet inet, int value) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Add))); + + /// + /// Subtracts the from the . + /// + /// The instance. + /// The inet. + /// The value to subtract. + /// + /// The augmented by the . + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet Subtract(this DbFunctions _, GaussDBInet inet, long value) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Subtract))); + + /// + /// Subtracts one from another . + /// + /// The instance. + /// The inet from which to subtract. + /// The inet to subtract. + /// + /// The numeric difference between the two given addresses. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static int Subtract(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Subtract))); + + #endregion + + #region Functions + + /// + /// Returns the abbreviated display format as text. + /// + /// The instance. + /// The inet to abbreviate. + /// + /// The abbreviated display format as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static string Abbreviate(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Abbreviate))); + + /// + /// Returns the abbreviated display format as text. + /// + /// The instance. + /// The cidr to abbreviate. + /// + /// The abbreviated display format as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete] + public static string Abbreviate(this DbFunctions _, GaussDBCidr cidr) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Abbreviate))); + + /// + /// Returns the broadcast address for a network. + /// + /// The instance. + /// The inet used to derive the broadcast address. + /// + /// The broadcast address for a network. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet Broadcast(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Broadcast))); + + /// + /// Extracts the family of an address; 4 for IPv4, 6 for IPv6. + /// + /// The instance. + /// The inet used to derive the family. + /// + /// The family of an address; 4 for IPv4, 6 for IPv6. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static int Family(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Family))); + + /// + /// Extracts the host (i.e. the IP address) as text. + /// + /// The instance. + /// The inet from which to extract the host. + /// + /// The host (i.e. the IP address) as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static string Host(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Host))); + + /// + /// Constructs the host mask for the network. + /// + /// The instance. + /// The inet used to construct the host mask. + /// + /// The constructed host mask. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet HostMask(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(HostMask))); + + /// + /// Extracts the length of the subnet mask. + /// + /// The instance. + /// The inet used to extract the subnet length. + /// + /// The length of the subnet mask. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static int MaskLength(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(MaskLength))); + + /// + /// Constructs the subnet mask for the network. + /// + /// The instance. + /// The inet used to construct the subnet mask. + /// + /// The subnet mask for the network. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet Netmask(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Netmask))); + + /// + /// Extracts the network part of the address. + /// + /// The instance. + /// The inet used to extract the network. + /// + /// The network part of the address. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete] + public static GaussDBCidr Network(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Network))); + + /// + /// Sets the length of the subnet mask. + /// + /// The instance. + /// The inet to modify. + /// The subnet mask length to set. + /// + /// The network with a subnet mask of the specified length. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBInet SetMaskLength(this DbFunctions _, GaussDBInet inet, int length) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SetMaskLength))); + + /// + /// Sets the length of the subnet mask. + /// + /// The instance. + /// The cidr to modify. + /// The subnet mask length to set. + /// + /// The network with a subnet mask of the specified length. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete] + public static GaussDBCidr SetMaskLength(this DbFunctions _, GaussDBCidr cidr, int length) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SetMaskLength))); + + /// + /// Extracts the IP address and subnet mask as text. + /// + /// The instance. + /// The inet to extract as text. + /// + /// The IP address and subnet mask as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static string Text(this DbFunctions _, GaussDBInet inet) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Text))); + + /// + /// Tests if the addresses are in the same family. + /// + /// The instance. + /// The primary inet. + /// The other inet. + /// + /// True if the addresses are in the same family; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool SameFamily(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SameFamily))); + + /// + /// Constructs the smallest network which includes both of the given networks. + /// + /// The instance. + /// The first inet. + /// The second inet. + /// + /// The smallest network which includes both of the given networks. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete] + public static GaussDBCidr Merge(this DbFunctions _, GaussDBInet inet, GaussDBInet other) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); + + /// + /// Sets the last 3 bytes of the MAC address to zero. For macaddr8, the last 5 bytes are set to zero. + /// + /// The instance. + /// The MAC address to truncate. + /// + /// The MAC address with the last 3 bytes set to zero. For macaddr8, the last 5 bytes are set to zero. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static PhysicalAddress Truncate(this DbFunctions _, PhysicalAddress macAddress) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Truncate))); + + /// + /// Sets the 7th bit to one, also known as modified EUI-64, for inclusion in an IPv6 address. + /// + /// The instance. + /// The MAC address to modify. + /// + /// The MAC address with the 7th bit set to one. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static PhysicalAddress Set7BitMac8(this DbFunctions _, PhysicalAddress macAddress) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Set7BitMac8))); + + #endregion + + #region Obsolete + + /// + /// Determines whether an (IPAddress Address, int Subnet) is less than another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The left-hand cidr. + /// The right-hand cidr. + /// + /// True if the (IPAddress Address, int Subnet) is less than the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool LessThan(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) is less than or equal to another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The left-hand cidr. + /// The right-hand cidr. + /// + /// True if the (IPAddress Address, int Subnet) is less than or equal to the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool LessThanOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) is greater than or equal to another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The left-hand cidr. + /// The right-hand cidr. + /// + /// True if the (IPAddress Address, int Subnet) is greater than or equal to the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool GreaterThanOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) is greater than another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The left-hand cidr. + /// The right-hand cidr. + /// + /// True if the (IPAddress Address, int Subnet) is greater than the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool GreaterThan(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an is contained within a network. + /// + /// The instance. + /// The inet to locate. + /// The cidr to search. + /// + /// True if the is contained within the network; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainedBy(this DbFunctions _, IPAddress inet, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether a network contains or is equal to another . + /// + /// The instance. + /// The network to search. + /// The IP address to locate. + /// + /// True if the network contains or is equal to the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainsOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, IPAddress other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) is contained within another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr to locate. + /// The cidr to search. + /// + /// True if the (IPAddress Address, int Subnet) is contained within the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainedBy(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an is contained within or equal to a network. + /// + /// The instance. + /// The inet to locate. + /// The cidr to search. + /// + /// True if the is contained within or equal to the network; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainedByOrEqual(this DbFunctions _, IPAddress inet, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) is contained within or equal to another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr to locate. + /// The cidr to search. + /// + /// True if the (IPAddress Address, int Subnet) is contained within or equal to the other (IPAddress Address, int Subnet); otherwise, + /// false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainedByOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether a network contains another . + /// + /// The instance. + /// The network to search. + /// The IP address to locate. + /// + /// True if the network contains the other ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool Contains( + this DbFunctions _, + (IPAddress Address, int Subnet) cidr, + IPAddress other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) contains another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr to search. + /// The cidr to locate. + /// + /// True if the (IPAddress Address, int Subnet) contains the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool Contains(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) contains or is equal to another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr to search. + /// The cidr to locate. + /// + /// True if the (IPAddress Address, int Subnet) contains or is equal to the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainsOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether a network contains or is contained by an . + /// + /// The instance. + /// The network to search. + /// The IP address to locate. + /// + /// True if the network contains or is contained by the ; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainsOrContainedBy(this DbFunctions _, (IPAddress Address, int Subnet) cidr, IPAddress other) + => throw new NotSupportedException(); + + /// + /// Determines whether an contains or is contained by a network. + /// + /// The instance. + /// The IP address to search. + /// The network to locate. + /// + /// True if the contains or is contained by the network; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainsOrContainedBy(this DbFunctions _, IPAddress inet, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Determines whether an (IPAddress Address, int Subnet) contains or is contained by another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr to search. + /// The cidr to locate. + /// + /// True if the (IPAddress Address, int Subnet) contains or is contained by the other (IPAddress Address, int Subnet); otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool ContainsOrContainedBy( + this DbFunctions _, + (IPAddress Address, int Subnet) cidr, + (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Computes the bitwise NOT operation on an (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr to negate. + /// + /// The result of the bitwise NOT operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) BitwiseNot(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Computes the bitwise AND of two (IPAddress Address, int Subnet) instances. + /// + /// The instance. + /// The left-hand cidr. + /// The right-hand cidr. + /// + /// The result of the bitwise AND operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) BitwiseAnd( + this DbFunctions _, + (IPAddress Address, int Subnet) cidr, + (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Computes the bitwise OR of two (IPAddress Address, int Subnet) instances. + /// + /// The instance. + /// The left-hand cidr. + /// The right-hand cidr. + /// + /// The result of the bitwise OR operation. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) BitwiseOr( + this DbFunctions _, + (IPAddress Address, int Subnet) cidr, + (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Adds the to the (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr. + /// The value to add. + /// + /// The (IPAddress Address, int Subnet) augmented by the . + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) Add(this DbFunctions _, (IPAddress Address, int Subnet) cidr, int value) + => throw new NotSupportedException(); + + /// + /// Subtracts the from the (IPAddress Address, int Subnet). + /// + /// The instance. + /// The inet. + /// The value to subtract. + /// + /// The (IPAddress Address, int Subnet) augmented by the . + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) Subtract(this DbFunctions _, (IPAddress Address, int Subnet) cidr, int value) + => throw new NotSupportedException(); + + /// + /// Subtracts one (IPAddress Address, int Subnet) from another (IPAddress Address, int Subnet). + /// + /// The instance. + /// The cidr from which to subtract. + /// The cidr to subtract. + /// + /// The difference between the two addresses. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static int Subtract( + this DbFunctions _, + (IPAddress Address, int Subnet) cidr, + (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Returns the abbreviated display format as text. + /// + /// The instance. + /// The cidr to abbreviate. + /// + /// The abbreviated display format as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static string Abbreviate(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Returns the broadcast address for a network. + /// + /// The instance. + /// The cidr used to derive the broadcast address. + /// + /// The broadcast address for a network. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static IPAddress Broadcast(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Extracts the family of an address; 4 for IPv4, 6 for IPv6. + /// + /// The instance. + /// The cidr used to derive the family. + /// + /// The family of an address; 4 for IPv4, 6 for IPv6. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static int Family(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Extracts the host (i.e. the IP address) as text. + /// + /// The instance. + /// The cidr from which to extract the host. + /// + /// The host (i.e. the IP address) as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static string Host(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Constructs the host mask for the network. + /// + /// The instance. + /// The cidr used to construct the host mask. + /// + /// The constructed host mask. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static IPAddress HostMask(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Extracts the length of the subnet mask. + /// + /// The instance. + /// The cidr used to extract the subnet length. + /// + /// The length of the subnet mask. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static int MaskLength(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Constructs the subnet mask for the network. + /// + /// The instance. + /// The cidr used to construct the subnet mask. + /// + /// The subnet mask for the network. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static IPAddress Netmask(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Extracts the network part of the address. + /// + /// The instance. + /// The cidr used to extract the network. + /// + /// The network part of the address. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) Network(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Sets the length of the subnet mask. + /// + /// The instance. + /// The cidr to modify. + /// The subnet mask length to set. + /// + /// The network with a subnet mask of the specified length. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) SetMaskLength(this DbFunctions _, (IPAddress Address, int Subnet) cidr, int length) + => throw new NotSupportedException(); + + /// + /// Extracts the IP address and subnet mask as text. + /// + /// The instance. + /// The cidr to extract as text. + /// + /// The IP address and subnet mask as text. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static string Text(this DbFunctions _, (IPAddress Address, int Subnet) cidr) + => throw new NotSupportedException(); + + /// + /// Tests if the addresses are in the same family. + /// + /// The instance. + /// The primary cidr. + /// The other cidr. + /// + /// True if the addresses are in the same family; otherwise, false. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static bool SameFamily(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + /// + /// Constructs the smallest network which includes both of the given networks. + /// + /// The instance. + /// The first cidr. + /// The second cidr. + /// + /// The smallest network which includes both of the given networks. + /// + /// + /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + [Obsolete("Use the overload which accepts GaussDBCidr", error: true)] + public static (IPAddress Address, int Subnet) Merge( + this DbFunctions _, + (IPAddress Address, int Subnet) cidr, + (IPAddress Address, int Subnet) other) + => throw new NotSupportedException(); + + #endregion Obsolete +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBRangeDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBRangeDbFunctionsExtensions.cs new file mode 100644 index 0000000000..7e633abb21 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBRangeDbFunctionsExtensions.cs @@ -0,0 +1,266 @@ +// ReSharper disable once CheckNamespace + +using HuaweiCloud.GaussDBTypes; + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods for supporting GaussDB translation. +/// +public static class GaussDBCidrDbFunctionsExtensions +{ + /// + /// Determines whether a range contains a specified value. + /// + /// The range in which to locate the value. + /// The value to locate in the range. + /// The type of the elements of . + /// + /// true + /// if the range contains the specified value; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Contains(this GaussDBRange range, T value) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a range contains a specified range. + /// + /// The range in which to locate the specified range. + /// The specified range to locate in the range. + /// The type of the elements of . + /// + /// true + /// if the range contains the specified range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ + /// query. + /// + public static bool Contains(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); + + /// + /// Determines whether a range is contained by a specified range. + /// + /// The specified range to locate in the range. + /// The range in which to locate the specified range. + /// The type of the elements of . + /// + /// true + /// if the range contains the specified range; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool ContainedBy(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); + + /// + /// Determines whether a range overlaps another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true + /// if the ranges overlap (share points in common); otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool Overlaps(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); + + /// + /// Determines whether a range is strictly to the left of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true + /// if the first range is strictly to the left of the second; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool IsStrictlyLeftOf(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); + + /// + /// Determines whether a range is strictly to the right of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true + /// if the first range is strictly to the right of the second; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool IsStrictlyRightOf(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); + + /// + /// Determines whether a range does not extend to the left of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true + /// if the first range does not extend to the left of the second; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool DoesNotExtendLeftOf(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); + + /// + /// Determines whether a range does not extend to the right of another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true + /// if the first range does not extend to the right of the second; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool DoesNotExtendRightOf(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); + + /// + /// Determines whether a range is adjacent to another range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// true + /// if the ranges are adjacent; otherwise, + /// false + /// . + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static bool IsAdjacentTo(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); + + /// + /// Returns the set union, which means unique elements that appear in either of two ranges. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// A range containing the unique elements that appear in either range. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBRange Union(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); + + /// + /// Returns the set intersection, which means elements that appear in each of two ranges. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// A range containing the elements that appear in both ranges. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBRange Intersect(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); + + /// + /// Returns the set difference, which means the elements of one range that do not appear in a second range. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// The elements that appear in the first range, but not the second range. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBRange Except(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Except))); + + /// + /// Returns the smallest range which includes both of the given ranges. + /// + /// The first range. + /// The second range. + /// The type of the elements of . + /// + /// The smallest range which includes both of the given ranges. + /// + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBRange Merge(this GaussDBRange a, GaussDBRange b) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); + + /// + /// Computes the union of the non-null input ranges. Corresponds to the GaussDB range_agg aggregate function. + /// + /// The ranges to be aggregated via union into a multirange. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core LINQ query. + /// + public static GaussDBRange[] RangeAgg(this IEnumerable> input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeAgg))); + + /// + /// Computes the intersection of the non-null input ranges. Corresponds to the GaussDB range_intersect_agg aggregate function. + /// + /// The ranges on which to perform the intersection operation. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static GaussDBRange RangeIntersectAgg(this IEnumerable> input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); + + /// + /// Computes the intersection of the non-null input multiranges. + /// Corresponds to the GaussDB range_intersect_agg aggregate function. + /// + /// The multiranges on which to perform the intersection operation. + /// GaussDB documentation for aggregate functions. + /// + /// is only intended for use via SQL translation as part of an EF Core + /// LINQ query. + /// + public static GaussDBRange[] RangeIntersectAgg(this IEnumerable[]> input) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); +} diff --git a/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBTrigramsDbFunctionsExtensions.cs b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBTrigramsDbFunctionsExtensions.cs new file mode 100644 index 0000000000..26c252bdd1 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/DbFunctionsExtensions/GaussDBTrigramsDbFunctionsExtensions.cs @@ -0,0 +1,166 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. +/// The methods on this class are accessed via . +/// +/// +/// See Database functions. +/// +public static class GaussDBTrigramsDbFunctionsExtensions +{ + /// + /// Returns an array of all the trigrams in the given . + /// (In practice this is seldom useful except for debugging.) + /// + /// + /// The method call is translated to show_trgm(text). + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static string[] TrigramsShow(this DbFunctions _, string text) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsShow))); + + /// + /// Returns a number that indicates how similar the two arguments are. + /// The range of the result is zero (indicating that the two strings are + /// completely dissimilar) to one (indicating that the two strings are identical). + /// + /// + /// The method call is translated to similarity(source, target). + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsSimilarity(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsSimilarity))); + + /// + /// Returns a number that indicates the greatest similarity between the set of trigrams + /// in the first string and any continuous extent of an ordered set of trigrams + /// in the second string. + /// + /// + /// The method call is translated to word_similarity(source, target). + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsWordSimilarity(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsWordSimilarity))); + + /// + /// Same as word_similarity(text, text), but forces extent boundaries to match word boundaries. + /// Since we don't have cross-word trigrams, this function actually returns greatest similarity + /// between first string and any continuous extent of words of the second string. + /// + /// + /// The method call is translated to strict_word_similarity(source, target). + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsStrictWordSimilarity(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsStrictWordSimilarity))); + + /// + /// Returns true if its arguments have a similarity that is greater than the current similarity + /// threshold set by pg_trgm.similarity_threshold. + /// + /// + /// The method call is translated to source % target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static bool TrigramsAreSimilar(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreSimilar))); + + /// + /// Returns true if the similarity between the trigram set in the first argument and a continuous + /// extent of an ordered trigram set in the second argument is greater than the current word similarity + /// threshold set by pg_trgm.word_similarity_threshold parameter. + /// + /// + /// The method call is translated to source <% target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static bool TrigramsAreWordSimilar(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreWordSimilar))); + + /// + /// Commutator of the <% operator. + /// + /// + /// The method call is translated to source %> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static bool TrigramsAreNotWordSimilar(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreNotWordSimilar))); + + /// + /// Returns true if its second argument has a continuous extent of an ordered trigram set that + /// matches word boundaries, and its similarity to the trigram set of the first argument is greater + /// than the current strict word similarity threshold set by the pg_trgm.strict_word_similarity_threshold + /// parameter. + /// + /// + /// The method call is translated to source <<% target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static bool TrigramsAreStrictWordSimilar(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreStrictWordSimilar))); + + /// + /// Commutator of the <<% operator. + /// + /// + /// The method call is translated to source %>> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static bool TrigramsAreNotStrictWordSimilar(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreNotStrictWordSimilar))); + + /// + /// Returns the "distance" between the arguments, that is one minus the similarity() value. + /// + /// + /// The method call is translated to source <-> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsSimilarityDistance(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsSimilarityDistance))); + + /// + /// Returns the "distance" between the arguments, that is one minus the word_similarity() value. + /// + /// + /// The method call is translated to source <<-> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsWordSimilarityDistance(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsWordSimilarityDistance))); + + /// + /// Commutator of the <<-> operator. + /// + /// + /// The method call is translated to source <->> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsWordSimilarityDistanceInverted(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsWordSimilarityDistanceInverted))); + + /// + /// Returns the "distance" between the arguments, that is one minus the strict_word_similarity() value. + /// + /// + /// The method call is translated to source <<<-> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsStrictWordSimilarityDistance(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsStrictWordSimilarityDistance))); + + /// + /// Commutator of the <<<-> operator. + /// + /// + /// The method call is translated to source <->>> target. + /// See https://www.postgresql.org/docs/current/pgtrgm.html. + /// + public static double TrigramsStrictWordSimilarityDistanceInverted(this DbFunctions _, string source, string target) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsStrictWordSimilarityDistanceInverted))); +} diff --git a/src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs b/src/EFCore.GaussDB/Extensions/ExpressionVisitorExtensions.cs similarity index 100% rename from src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs rename to src/EFCore.GaussDB/Extensions/ExpressionVisitorExtensions.cs diff --git a/src/EFCore.GaussDB/Extensions/GaussDBAlterDatabaseOperationAnnotations.cs b/src/EFCore.GaussDB/Extensions/GaussDBAlterDatabaseOperationAnnotations.cs new file mode 100644 index 0000000000..a3cb4ed720 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/GaussDBAlterDatabaseOperationAnnotations.cs @@ -0,0 +1,95 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Extension methods for for GaussDB-specific metadata. +/// +public static class GaussDBAlterDatabaseOperationExtensions +{ + /// + /// 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 IReadOnlyList GetPostgresCollations(this AlterDatabaseOperation operation) + => GaussDBCollation.GetCollations(operation).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 static IReadOnlyList GetOldPostgresCollations(this AlterDatabaseOperation operation) + => GaussDBCollation.GetCollations(operation.OldDatabase).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 static IReadOnlyList GetPostgresExtensions(this AlterDatabaseOperation operation) + => GaussDBExtension.GetPostgresExtensions(operation).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 static IReadOnlyList GetOldPostgresExtensions(this AlterDatabaseOperation operation) + => GaussDBExtension.GetPostgresExtensions(operation.OldDatabase).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 static IReadOnlyList GetPostgresEnums(this AlterDatabaseOperation operation) + => GaussDBEnum.GetPostgresEnums(operation).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 static IReadOnlyList GetOldPostgresEnums(this AlterDatabaseOperation operation) + => GaussDBEnum.GetPostgresEnums(operation.OldDatabase).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 static IReadOnlyList GetPostgresRanges(this AlterDatabaseOperation operation) + => HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.GaussDBRange.GetPostgresRanges(operation).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 static IReadOnlyList GetOldPostgresRanges(this AlterDatabaseOperation operation) + => HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.GaussDBRange.GetPostgresRanges(operation.OldDatabase).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 static GaussDBExtension GetOrAddPostgresExtension( + this AlterDatabaseOperation operation, + string? schema, + string name, + string? version) + => GaussDBExtension.GetOrAddPostgresExtension(operation, schema, name, version); +} diff --git a/src/EFCore.GaussDB/Extensions/GaussDBDatabaseFacadeExtensions.cs b/src/EFCore.GaussDB/Extensions/GaussDBDatabaseFacadeExtensions.cs new file mode 100644 index 0000000000..dd4b384d17 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/GaussDBDatabaseFacadeExtensions.cs @@ -0,0 +1,56 @@ +using System.Data.Common; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// GaussDB specific extension methods for . +/// +public static class GaussDBDatabaseFacadeExtensions +{ + /// + /// + /// Returns true if the database provider currently in use is the GaussDB provider. + /// + /// + /// This method can only be used after the has been configured because + /// it is only then that the provider is known. This means that this method cannot be used + /// in because this is where application code sets the + /// provider to use as part of configuring the context. + /// + /// + /// The facade from . + /// True if GaussDB is being used; false otherwise. + public static bool IsGaussDB(this DatabaseFacade database) + => database.ProviderName == typeof(GaussDBOptionsExtension).GetTypeInfo().Assembly.GetName().Name; + + /// + /// Sets the underlying configured for this . + /// + /// + /// + /// It may not be possible to change the data source if existing connection, if any, is open. + /// + /// + /// See Connections and connection strings for more information and examples. + /// + /// + /// The for the context. + /// The connection string. + public static void SetDbDataSource(this DatabaseFacade databaseFacade, DbDataSource dataSource) + => ((GaussDBRelationalConnection)GetFacadeDependencies(databaseFacade).RelationalConnection).DbDataSource = dataSource; + + private static IRelationalDatabaseFacadeDependencies GetFacadeDependencies(DatabaseFacade databaseFacade) + { + var dependencies = ((IDatabaseFacadeDependenciesAccessor)databaseFacade).Dependencies; + + if (dependencies is IRelationalDatabaseFacadeDependencies relationalDependencies) + { + return relationalDependencies; + } + + throw new InvalidOperationException(RelationalStrings.RelationalNotInUse); + } +} diff --git a/src/EFCore.GaussDB/Extensions/GaussDBDatabaseModelExtensions.cs b/src/EFCore.GaussDB/Extensions/GaussDBDatabaseModelExtensions.cs new file mode 100644 index 0000000000..2207d84b67 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/GaussDBDatabaseModelExtensions.cs @@ -0,0 +1,44 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// 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 GaussDBDatabaseModelExtensions +{ + /// + /// 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 GaussDBExtension GetOrAddPostgresExtension( + this DatabaseModel model, + string? schema, + string name, + string? version) + => GaussDBExtension.GetOrAddPostgresExtension(model, schema, name, 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. + /// + public static IReadOnlyList GetPostgresExtensions(this DatabaseModel model) + => GaussDBExtension.GetPostgresExtensions(model).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 static IReadOnlyList GetPostgresEnums(this DatabaseModel model) + => GaussDBEnum.GetPostgresEnums(model).ToArray(); +} diff --git a/src/EFCore.GaussDB/Extensions/GaussDBDbContextOptionsBuilderExtensions.cs b/src/EFCore.GaussDB/Extensions/GaussDBDbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000000..348206e1a0 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/GaussDBDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,277 @@ +using System.Data.Common; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods on and +/// used to configure a to context to a GaussDB database with GaussDB. +/// +public static class GaussDBDbContextOptionsBuilderExtensions +{ + /// + /// + /// Configures the context to connect to a GaussDB server with GaussDB, but without initially setting any + /// or connection string. + /// + /// + /// The connection or connection string must be set before the is used to connect + /// to a database. Set a connection using . + /// Set a connection string using . + /// + /// + /// The builder being used to configure the context. + /// An optional action to allow additional GaussDB-specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + Action? npgsqlOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(GetOrCreateExtension(optionsBuilder)); + + ConfigureWarnings(optionsBuilder); + + npgsqlOptionsAction?.Invoke(new GaussDBDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// A builder for setting options on the context. + /// The connection string of the database to connect to. + /// An optional action to allow additional GaussDB-specific configuration. + /// + /// The options builder so that further configuration can be chained. + /// + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + string? connectionString, + Action? npgsqlOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + var extension = (GaussDBOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnectionString(connectionString); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + ConfigureWarnings(optionsBuilder); + + npgsqlOptionsAction?.Invoke(new GaussDBDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. The caller owns the connection and is + /// responsible for its disposal. + /// + /// An optional action to allow additional GaussDB-specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + Action? npgsqlOptionsAction = null) + => UseGaussDB(optionsBuilder, connection, contextOwnsConnection: false, npgsqlOptionsAction); + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// A builder for setting options on the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// An optional action to allow additional GaussDB-specific configuration. + /// + /// The options builder so that further configuration can be chained. + /// + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + bool contextOwnsConnection, + Action? npgsqlOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + Check.NotNull(connection, nameof(connection)); + + var extension = (GaussDBOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnection(connection, contextOwnsConnection); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + ConfigureWarnings(optionsBuilder); + + npgsqlOptionsAction?.Invoke(new GaussDBDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// A builder for setting options on the context. + /// A which will be used to get database connections. + /// An optional action to allow additional GaussDB-specific configuration. + /// + /// The options builder so that further configuration can be chained. + /// + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + DbDataSource dataSource, + Action? npgsqlOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + Check.NotNull(dataSource, nameof(dataSource)); + + var extension = (GaussDBOptionsExtension)GetOrCreateExtension(optionsBuilder).WithDataSource(dataSource); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + ConfigureWarnings(optionsBuilder); + + npgsqlOptionsAction?.Invoke(new GaussDBDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + + /// + /// + /// Configures the context to connect to a GaussDB server with GaussDB, but without initially setting any + /// , or connection string. + /// + /// + /// The connection, data source or connection string must be set explicitly or registered in the DI + /// before the is used to connect to a database. + /// Set a connection using , a data source using + /// , or a connection string using + /// . + /// + /// + /// The builder being used to configure the context. + /// An optional action to allow additional GaussDB-specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + Action? npgsqlOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseGaussDB( + (DbContextOptionsBuilder)optionsBuilder, npgsqlOptionsAction); + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// A builder for setting options on the context. + /// The connection string of the database to connect to. + /// An optional action to allow additional GaussDB-configuration. + /// + /// The options builder so that further configuration can be chained. + /// + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + string? connectionString, + Action? npgsqlOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseGaussDB( + (DbContextOptionsBuilder)optionsBuilder, connectionString, npgsqlOptionsAction); + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// A builder for setting options on the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. The caller owns the connection and is + /// responsible for its disposal. + /// + /// An optional action to allow additional GaussDB-specific configuration. + /// + /// The options builder so that further configuration can be chained. + /// + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + Action? npgsqlOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseGaussDB( + (DbContextOptionsBuilder)optionsBuilder, connection, npgsqlOptionsAction); + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// The type of context to be configured. + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// An optional action to allow additional GaussDB-specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + bool contextOwnsConnection, + Action? npgsqlOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseGaussDB( + (DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, npgsqlOptionsAction); + + /// + /// Configures the context to connect to a GaussDB database with GaussDB. + /// + /// A builder for setting options on the context. + /// A which will be used to get database connections. + /// An optional action to allow additional GaussDB-specific configuration. + /// + /// The options builder so that further configuration can be chained. + /// + public static DbContextOptionsBuilder UseGaussDB( + this DbContextOptionsBuilder optionsBuilder, + DbDataSource dataSource, + Action? npgsqlOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseGaussDB( + (DbContextOptionsBuilder)optionsBuilder, dataSource, npgsqlOptionsAction); + + /// + /// Returns an existing instance of , or a new instance if one does not exist. + /// + /// The to search. + /// + /// An existing instance of , or a new instance if one does not exist. + /// + private static GaussDBOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.Options.FindExtension() is { } existing + ? new GaussDBOptionsExtension(existing) + : new GaussDBOptionsExtension(); + + private static void ConfigureWarnings(DbContextOptionsBuilder optionsBuilder) + { + var coreOptionsExtension = optionsBuilder.Options.FindExtension() + ?? new CoreOptionsExtension(); + + coreOptionsExtension = RelationalOptionsExtension.WithDefaultWarningConfiguration(coreOptionsExtension); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(coreOptionsExtension); + } +} diff --git a/src/EFCore.GaussDB/Extensions/GaussDBMigrationBuilderExtensions.cs b/src/EFCore.GaussDB/Extensions/GaussDBMigrationBuilderExtensions.cs new file mode 100644 index 0000000000..6ca8e0dd93 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/GaussDBMigrationBuilderExtensions.cs @@ -0,0 +1,47 @@ +// ReSharper disable once CheckNamespace + +namespace Microsoft.EntityFrameworkCore; + +/// +/// 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 GaussDBMigrationBuilderExtensions +{ + /// + /// Returns true if the active provider in a migration is the GaussDB provider. + /// + /// The migrationBuilder from the parameters on + /// + /// or + /// + /// . + /// True if GaussDB is being used; false otherwise. + public static bool IsGaussDB(this MigrationBuilder builder) + => builder.ActiveProvider == typeof(GaussDBMigrationBuilderExtensions).GetTypeInfo().Assembly.GetName().Name; + + /// + /// 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 MigrationBuilder EnsurePostgresExtension( + this MigrationBuilder builder, + string name, + string? schema = null, + string? version = null) + { + Check.NotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NullButNotEmpty(version, nameof(schema)); + + var op = new AlterDatabaseOperation(); + op.GetOrAddPostgresExtension(schema, name, version); + builder.Operations.Add(op); + + return builder; + } +} diff --git a/src/EFCore.GaussDB/Extensions/GaussDBServiceCollectionExtensions.cs b/src/EFCore.GaussDB/Extensions/GaussDBServiceCollectionExtensions.cs new file mode 100644 index 0000000000..4dc8c7ec25 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/GaussDBServiceCollectionExtensions.cs @@ -0,0 +1,133 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Update.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides extension methods to configure Entity Framework Core for GaussDB. +/// +// ReSharper disable once UnusedMember.Global +public static class GaussDBServiceCollectionExtensions +{ + /// + /// + /// Registers the given Entity Framework context as a service in the + /// and configures it to connect to a GaussDB database. + /// + /// + /// Use this method when using dependency injection in your application, such as with ASP.NET Core. + /// For applications that don't use dependency injection, consider creating + /// instances directly with its constructor. The method can then be + /// overridden to configure the SQL Server provider and connection string. + /// + /// + /// To configure the for the context, either override the + /// method in your derived context, or supply + /// an optional action to configure the for the context. + /// + /// + /// For more information on how to use this method, see the Entity Framework Core documentation at https://aka.ms/efdocs. + /// For more information on using dependency injection, see https://go.microsoft.com/fwlink/?LinkId=526890. + /// + /// + /// The type of context to be registered. + /// The to add services to. + /// The connection string of the database to connect to. + /// An optional action to allow additional SQL Server specific configuration. + /// An optional action to configure the for the context. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddGaussDB( + this IServiceCollection serviceCollection, + string? connectionString, + Action? npgsqlOptionsAction = null, + Action? optionsAction = null) + where TContext : DbContext + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + return serviceCollection.AddDbContext( + (_, options) => + { + optionsAction?.Invoke(options); + options.UseGaussDB(connectionString, npgsqlOptionsAction); + }); + } + + /// + /// + /// Adds the services required by the GaussDB database provider for Entity Framework + /// to an . + /// + /// + /// Calling this method is no longer necessary when building most applications, including those that + /// use dependency injection in ASP.NET or elsewhere. + /// It is only needed when building the internal service provider for use with + /// the method. + /// This is not recommend other than for some advanced scenarios. + /// + /// + /// The to add services to. + /// + /// The same service collection so that multiple calls can be chained. + /// + public static IServiceCollection AddEntityFrameworkGaussDB(this IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + new EntityFrameworkGaussDBServicesBuilder(serviceCollection) + .TryAdd() + .TryAdd>() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAddProviderSpecificServices( + b => b + .TryAddSingleton() + .TryAddSingleton() + .TryAddSingleton() + .TryAddSingleton() + .TryAddScoped()) + .TryAddCoreServices(); + + return serviceCollection; + } +} diff --git a/src/EFCore.GaussDB/Extensions/Internal/GaussDBShapedQueryExpressionExtensions.cs b/src/EFCore.GaussDB/Extensions/Internal/GaussDBShapedQueryExpressionExtensions.cs new file mode 100644 index 0000000000..c48805cbc3 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/Internal/GaussDBShapedQueryExpressionExtensions.cs @@ -0,0 +1,158 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBShapedQueryExpressionExtensions +{ + /// + /// 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: [GaussDBUnnestExpression { 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 GaussDBNewArrayExpression(elements, valuesExpression.RowValues[0].Values[1].Type.MakeArrayType(), typeMapping: null); + return true; + } + + array = null; + return false; + } + + /// + /// Checks whether the given expression maps to a GaussDB array, as opposed to a multirange type. + /// + private static bool IsPostgresArray(SqlExpression expression) + => expression switch + { + { TypeMapping: GaussDBArrayTypeMapping } => true, + { TypeMapping: GaussDBMultirangeTypeMapping } => 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/MemberInfoExtensions.cs b/src/EFCore.GaussDB/Extensions/MemberInfoExtensions.cs similarity index 100% rename from src/EFCore.PG/Extensions/MemberInfoExtensions.cs rename to src/EFCore.GaussDB/Extensions/MemberInfoExtensions.cs diff --git a/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBEntityTypeExtensions.cs b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBEntityTypeExtensions.cs new file mode 100644 index 0000000000..c7a9472fd7 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBEntityTypeExtensions.cs @@ -0,0 +1,118 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Extension methods for for GaussDB-specific metadata. +/// +public static class GaussDBEntityTypeExtensions +{ + #region Storage parameters + + /// + /// Gets all storage parameters for the table mapped to the entity type. + /// + public static Dictionary GetStorageParameters(this IReadOnlyEntityType entityType) + => entityType.GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) + .ToDictionary( + a => a.Name.Substring(GaussDBAnnotationNames.StorageParameterPrefix.Length), + a => a.Value); + + /// + /// Gets a storage parameter for the table mapped to the entity type. + /// + public static string? GetStorageParameter(this IEntityType entityType, string parameterName) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + return (string?)entityType[GaussDBAnnotationNames.StorageParameterPrefix + parameterName]; + } + + /// + /// Sets a storage parameter on the table mapped to the entity type. + /// + public static void SetStorageParameter(this IMutableEntityType entityType, string parameterName, object? parameterValue) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + entityType.SetOrRemoveAnnotation(GaussDBAnnotationNames.StorageParameterPrefix + parameterName, parameterValue); + } + + /// + /// Sets a storage parameter on the table mapped to the entity type. + /// + public static object SetStorageParameter( + this IConventionEntityType entityType, + string parameterName, + object? parameterValue, + bool fromDataAnnotation = false) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + entityType.SetOrRemoveAnnotation(GaussDBAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); + + return parameterName; + } + + /// + /// Gets the configuration source for a storage parameter for the table mapped to the entity type. + /// + public static ConfigurationSource? GetStorageParameterConfigurationSource( + this IConventionEntityType index, + string parameterName) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + return index.FindAnnotation(GaussDBAnnotationNames.StorageParameterPrefix + parameterName)?.GetConfigurationSource(); + } + + #endregion Storage parameters + + #region Unlogged + + /// + /// Gets whether the table to which the entity is mapped is unlogged. + /// + public static bool GetIsUnlogged(this IReadOnlyEntityType entityType) + => entityType[GaussDBAnnotationNames.UnloggedTable] as bool? ?? false; + + /// + /// Sets whether the table to which the entity is mapped is unlogged. + /// + public static void SetIsUnlogged(this IMutableEntityType entityType, bool unlogged) + => entityType.SetOrRemoveAnnotation(GaussDBAnnotationNames.UnloggedTable, unlogged); + + /// + /// Sets whether the table to which the entity is mapped is unlogged. + /// + public static bool SetIsUnlogged( + this IConventionEntityType entityType, + bool unlogged, + bool fromDataAnnotation = false) + { + entityType.SetOrRemoveAnnotation(GaussDBAnnotationNames.UnloggedTable, unlogged, fromDataAnnotation); + + return unlogged; + } + + /// + /// Gets the configuration source for whether the table to which the entity is mapped is unlogged. + /// + public static ConfigurationSource? GetIsUnloggedConfigurationSource(this IConventionEntityType index) + => index.FindAnnotation(GaussDBAnnotationNames.UnloggedTable)?.GetConfigurationSource(); + + #endregion Unlogged + + #region CockroachDb interleave in parent + + /// + /// Gets the CockroachDB-specific interleave-in-parent setting for the table to which the entity is mapped. + /// + public static CockroachDbInterleaveInParent GetCockroachDbInterleaveInParent(this IReadOnlyEntityType entityType) + => new(entityType); + + #endregion CockroachDb interleave in parent +} diff --git a/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBIndexExtensions.cs b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBIndexExtensions.cs new file mode 100644 index 0000000000..a5fba70294 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBIndexExtensions.cs @@ -0,0 +1,482 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Extension methods for for GaussDB-specific metadata. +/// +public static class GaussDBIndexExtensions +{ + #region Method + + /// + /// Returns the index method to be used, or null if it hasn't been specified. + /// null selects the default (currently btree). + /// + /// + /// http://www.postgresql.org/docs/current/static/sql-createindex.html + /// + public static string? GetMethod(this IReadOnlyIndex index) + => (string?)index[GaussDBAnnotationNames.IndexMethod]; + + /// + /// Sets the index method to be used, or null if it hasn't been specified. + /// null selects the default (currently btree). + /// + /// + /// http://www.postgresql.org/docs/current/static/sql-createindex.html + /// + public static void SetMethod(this IMutableIndex index, string? method) + => index.SetOrRemoveAnnotation(GaussDBAnnotationNames.IndexMethod, method); + + /// + /// Sets the index method to be used, or null if it hasn't been specified. + /// null selects the default (currently btree). + /// + /// + /// http://www.postgresql.org/docs/current/static/sql-createindex.html + /// + public static string? SetMethod( + this IConventionIndex index, + string? method, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(method, nameof(method)); + + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.IndexMethod, method, fromDataAnnotation); + + return method; + } + + /// + /// Returns the for the index method. + /// + /// The index. + /// The for the index method. + public static ConfigurationSource? GetMethodConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.IndexMethod)?.GetConfigurationSource(); + + #endregion Method + + #region Operators + + /// + /// Returns the column operators to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + public static IReadOnlyList? GetOperators(this IReadOnlyIndex index) + => (IReadOnlyList?)index[GaussDBAnnotationNames.IndexOperators]; + + /// + /// Sets the column operators to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + public static void SetOperators(this IMutableIndex index, IReadOnlyList? operators) + => index.SetOrRemoveAnnotation(GaussDBAnnotationNames.IndexOperators, operators); + + /// + /// Sets the column operators to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + public static IReadOnlyList? SetOperators( + this IConventionIndex index, + IReadOnlyList? operators, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(operators, nameof(operators)); + + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.IndexOperators, operators, fromDataAnnotation); + + return operators; + } + + /// + /// Returns the for the index operators. + /// + /// The index. + /// The for the index operators. + public static ConfigurationSource? GetOperatorsConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.IndexOperators)?.GetConfigurationSource(); + + #endregion Operators + + #region Collation + + /// + /// Returns the column collations to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-collations.html + /// +#pragma warning disable 618 + public static IReadOnlyList? GetCollation(this IReadOnlyIndex index) + => (IReadOnlyList?)( + index[RelationalAnnotationNames.Collation] ?? index[GaussDBAnnotationNames.IndexCollation]); +#pragma warning restore 618 + + /// + /// Sets the column collations to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-collations.html + /// + public static void SetCollation(this IMutableIndex index, IReadOnlyList? collations) + => index.SetOrRemoveAnnotation(RelationalAnnotationNames.Collation, collations); + + /// + /// Sets the column collations to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-collations.html + /// + public static IReadOnlyList? SetCollation( + this IConventionIndex index, + IReadOnlyList? collations, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(collations, nameof(collations)); + + index.SetOrRemoveAnnotation(RelationalAnnotationNames.Collation, collations, fromDataAnnotation); + + return collations; + } + + /// + /// Returns the for the index collations. + /// + /// The index. + /// The for the index collations. + public static ConfigurationSource? GetCollationConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource(); + + #endregion Collation + + #region Null sort order + + /// + /// Returns the column NULL sort orders to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-ordering.html + /// + public static IReadOnlyList? GetNullSortOrder(this IReadOnlyIndex index) + => (IReadOnlyList?)index[GaussDBAnnotationNames.IndexNullSortOrder]; + + /// + /// Sets the column NULL sort orders to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-ordering.html + /// + public static void SetNullSortOrder(this IMutableIndex index, IReadOnlyList? nullSortOrder) + => index.SetOrRemoveAnnotation(GaussDBAnnotationNames.IndexNullSortOrder, nullSortOrder); + + /// + /// Sets the column NULL sort orders to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-ordering.html + /// + public static IReadOnlyList? SetNullSortOrder( + this IConventionIndex index, + IReadOnlyList? nullSortOrder, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(nullSortOrder, nameof(nullSortOrder)); + + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.IndexNullSortOrder, nullSortOrder, fromDataAnnotation); + + return nullSortOrder; + } + + /// + /// Returns the for the index null sort orders. + /// + /// The index. + /// The for the index null sort orders. + public static ConfigurationSource? GetNullSortOrderConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.IndexNullSortOrder)?.GetConfigurationSource(); + + #endregion + + #region Included properties + + /// + /// Returns included property names, or null if they have not been specified. + /// + /// The index. + /// The included property names, or null if they have not been specified. + public static IReadOnlyList? GetIncludeProperties(this IReadOnlyIndex index) + => (IReadOnlyList?)index[GaussDBAnnotationNames.IndexInclude]; + + /// + /// Sets included property names. + /// + /// The index. + /// The value to set. + public static void SetIncludeProperties(this IMutableIndex index, IReadOnlyList? properties) + => index.SetOrRemoveAnnotation( + GaussDBAnnotationNames.IndexInclude, + properties); + + /// + /// Sets included property names. + /// + /// The index. + /// Indicates whether the configuration was specified using a data annotation. + /// The value to set. + public static IReadOnlyList? SetIncludeProperties( + this IConventionIndex index, + IReadOnlyList? properties, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(properties, nameof(properties)); + + index.SetOrRemoveAnnotation( + GaussDBAnnotationNames.IndexInclude, + properties, + fromDataAnnotation); + + return properties; + } + + /// + /// Returns the for the included property names. + /// + /// The index. + /// The for the included property names. + public static ConfigurationSource? GetIncludePropertiesConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.IndexInclude)?.GetConfigurationSource(); + + #endregion Included properties + + #region Created concurrently + + /// + /// Returns a value indicating whether the index is created concurrently. + /// + /// The index. + /// true if the index is created concurrently. + public static bool? IsCreatedConcurrently(this IReadOnlyIndex index) + => (bool?)index[GaussDBAnnotationNames.CreatedConcurrently]; + + /// + /// Sets a value indicating whether the index is created concurrently. + /// + /// The index. + /// The value to set. + public static void SetIsCreatedConcurrently(this IMutableIndex index, bool? createdConcurrently) + => index.SetOrRemoveAnnotation(GaussDBAnnotationNames.CreatedConcurrently, createdConcurrently); + + /// + /// Sets a value indicating whether the index is created concurrently. + /// + /// The index. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static bool? SetIsCreatedConcurrently( + this IConventionIndex index, + bool? createdConcurrently, + bool fromDataAnnotation = false) + { + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.CreatedConcurrently, createdConcurrently, fromDataAnnotation); + + return createdConcurrently; + } + + /// + /// Returns the for whether the index is created concurrently. + /// + /// The index. + /// The for whether the index is created concurrently. + public static ConfigurationSource? GetIsCreatedConcurrentlyConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.CreatedConcurrently)?.GetConfigurationSource(); + + #endregion Created concurrently + + #region NULLS distinct + + /// + /// Returns whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// http://www.postgresql.org/docs/current/static/sql-createindex.html + /// + public static bool? GetAreNullsDistinct(this IReadOnlyIndex index) + => (bool?)index[GaussDBAnnotationNames.NullsDistinct]; + + /// + /// Sets whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// http://www.postgresql.org/docs/current/static/sql-createindex.html + /// + public static void SetAreNullsDistinct(this IMutableIndex index, bool? nullsDistinct) + => index.SetOrRemoveAnnotation(GaussDBAnnotationNames.NullsDistinct, nullsDistinct); + + /// + /// Sets whether for a unique index, null values should be considered distinct (not equal). + /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. + /// + /// + /// http://www.postgresql.org/docs/current/static/sql-createindex.html + /// + public static bool? SetAreNullsDistinct(this IConventionIndex index, bool? nullsDistinct, bool fromDataAnnotation = false) + { + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.NullsDistinct, nullsDistinct, fromDataAnnotation); + + return nullsDistinct; + } + + /// + /// Returns the for whether nulls are considered distinct. + /// + /// The index. + /// The . + public static ConfigurationSource? GetAreNullsDistinctConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.NullsDistinct)?.GetConfigurationSource(); + + #endregion NULLS distinct + + #region ToTsVector + + /// + /// Returns the text search configuration for this tsvector expression index, or null if this is not a + /// tsvector expression index. + /// + /// The index. + /// + /// https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX + /// + public static string? GetTsVectorConfig(this IReadOnlyIndex index) + => (string?)index[GaussDBAnnotationNames.TsVectorConfig]; + + /// + /// Sets the text search configuration for this tsvector expression index, or null if this is not a + /// tsvector expression index. + /// + /// The index. + /// + /// + /// 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. + /// + /// + /// + /// https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX + /// + public static void SetTsVectorConfig(this IMutableIndex index, string? config) + => index.SetOrRemoveAnnotation(GaussDBAnnotationNames.TsVectorConfig, config); + + /// + /// Sets the index to tsvector config name to be used. + /// + /// The index. + /// + /// + /// 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. + /// + /// https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX + /// + public static string? SetTsVectorConfig( + this IConventionIndex index, + string? config, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(config, nameof(config)); + + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.TsVectorConfig, config, fromDataAnnotation); + + return config; + } + + /// + /// Returns the for the tsvector config. + /// + /// The index. + /// The for the tsvector config. + public static ConfigurationSource? GetTsVectorConfigConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(GaussDBAnnotationNames.TsVectorConfig)?.GetConfigurationSource(); + + #endregion ToTsVector + + #region Storage parameters + + /// + /// Gets all storage parameters for the index. + /// + public static Dictionary GetStorageParameters(this IReadOnlyIndex index) + => index.GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) + .ToDictionary( + a => a.Name.Substring(GaussDBAnnotationNames.StorageParameterPrefix.Length), + a => a.Value); + + /// + /// Gets a storage parameter for the index. + /// + public static string? GetStorageParameter(this IIndex index, string parameterName) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + return (string?)index[GaussDBAnnotationNames.StorageParameterPrefix + parameterName]; + } + + /// + /// Sets a storage parameter on the index. + /// + public static void SetStorageParameter(this IMutableIndex index, string parameterName, object? parameterValue) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.StorageParameterPrefix + parameterName, parameterValue); + } + + /// + /// Sets a storage parameter on the index. + /// + public static object SetStorageParameter( + this IConventionIndex index, + string parameterName, + object? parameterValue, + bool fromDataAnnotation = false) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + index.SetOrRemoveAnnotation(GaussDBAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); + + return parameterName; + } + + /// + /// Gets the configuration source for a storage parameter for the table mapped to the entity type. + /// + public static ConfigurationSource? GetStorageParameterConfigurationSource(this IConventionIndex index, string parameterName) + { + Check.NotEmpty(parameterName, nameof(parameterName)); + + return index.FindAnnotation(GaussDBAnnotationNames.StorageParameterPrefix + parameterName)?.GetConfigurationSource(); + } + + #endregion Storage parameters +} diff --git a/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBModelExtensions.cs b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBModelExtensions.cs new file mode 100644 index 0000000000..fee4c520b4 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBModelExtensions.cs @@ -0,0 +1,519 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Model extension methods for GaussDB-specific metadata. +/// +/// +/// See Modeling entity types and relationships. +/// +public static class GaussDBModelExtensions +{ + /// + /// The default name for the hi-lo sequence. + /// + public const string DefaultHiLoSequenceName = "EntityFrameworkHiLoSequence"; + + /// + /// The default prefix for sequences applied to properties. + /// + public const string DefaultSequenceNameSuffix = "Sequence"; + + #region HiLo + + /// + /// Returns the name to use for the default hi-lo sequence. + /// + /// The model. + /// The name to use for the default hi-lo sequence. + public static string GetHiLoSequenceName(this IReadOnlyModel model) + => (string?)model[GaussDBAnnotationNames.HiLoSequenceName] + ?? DefaultHiLoSequenceName; + + /// + /// Sets the name to use for the default hi-lo sequence. + /// + /// The model. + /// The value to set. + public static void SetHiLoSequenceName(this IMutableModel model, string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.HiLoSequenceName, name); + } + + /// + /// Sets the name to use for the default hi-lo sequence. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static string? SetHiLoSequenceName( + this IConventionModel model, + string? name, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(name, nameof(name)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.HiLoSequenceName, name, fromDataAnnotation); + + return name; + } + + /// + /// Returns the for the default hi-lo sequence name. + /// + /// The model. + /// The for the default hi-lo sequence name. + public static ConfigurationSource? GetHiLoSequenceNameConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.HiLoSequenceName)?.GetConfigurationSource(); + + /// + /// Returns the schema to use for the default hi-lo sequence. + /// + /// + /// The model. + /// The schema to use for the default hi-lo sequence. + public static string? GetHiLoSequenceSchema(this IReadOnlyModel model) + => (string?)model[GaussDBAnnotationNames.HiLoSequenceSchema]; + + /// + /// Sets the schema to use for the default hi-lo sequence. + /// + /// The model. + /// The value to set. + public static void SetHiLoSequenceSchema(this IMutableModel model, string? value) + { + Check.NullButNotEmpty(value, nameof(value)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema, value); + } + + /// + /// Sets the schema to use for the default hi-lo sequence. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static string? SetHiLoSequenceSchema( + this IConventionModel model, + string? value, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(value, nameof(value)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema, value, fromDataAnnotation); + + return value; + } + + /// + /// Returns the for the default hi-lo sequence schema. + /// + /// The model. + /// The for the default hi-lo sequence schema. + public static ConfigurationSource? GetHiLoSequenceSchemaConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema)?.GetConfigurationSource(); + + #endregion + + #region Sequence + + /// + /// Returns the suffix to append to the name of automatically created sequences. + /// + /// The model. + /// The name to use for the default key value generation sequence. + public static string GetSequenceNameSuffix(this IReadOnlyModel model) + => (string?)model[GaussDBAnnotationNames.SequenceNameSuffix] + ?? DefaultSequenceNameSuffix; + + /// + /// Sets the suffix to append to the name of automatically created sequences. + /// + /// The model. + /// The value to set. + public static void SetSequenceNameSuffix(this IMutableModel model, string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.SequenceNameSuffix, name); + } + + /// + /// Sets the suffix to append to the name of automatically created sequences. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceNameSuffix( + this IConventionModel model, + string? name, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(name, nameof(name)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.SequenceNameSuffix, name, fromDataAnnotation); + + return name; + } + + /// + /// Returns the for the default value generation sequence name suffix. + /// + /// The model. + /// The for the default key value generation sequence name. + public static ConfigurationSource? GetSequenceNameSuffixConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.SequenceNameSuffix)?.GetConfigurationSource(); + + /// + /// Returns the schema to use for the default value generation sequence. + /// + /// + /// The model. + /// The schema to use for the default key value generation sequence. + public static string? GetSequenceSchema(this IReadOnlyModel model) + => (string?)model[GaussDBAnnotationNames.SequenceSchema]; + + /// + /// Sets the schema to use for the default key value generation sequence. + /// + /// The model. + /// The value to set. + public static void SetSequenceSchema(this IMutableModel model, string? value) + { + Check.NullButNotEmpty(value, nameof(value)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.SequenceSchema, value); + } + + /// + /// Sets the schema to use for the default key value generation sequence. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceSchema( + this IConventionModel model, + string? value, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(value, nameof(value)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.SequenceSchema, value, fromDataAnnotation); + + return value; + } + + /// + /// Returns the for the default key value generation sequence schema. + /// + /// The model. + /// The for the default key value generation sequence schema. + public static ConfigurationSource? GetSequenceSchemaConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.SequenceSchema)?.GetConfigurationSource(); + + #endregion Sequence + + #region Value Generation Strategy + + /// + /// Returns the to use for properties + /// of keys in the model, unless the property has a strategy explicitly set. + /// + /// The model. + /// The default . + public static GaussDBValueGenerationStrategy? GetValueGenerationStrategy(this IReadOnlyModel model) + => (GaussDBValueGenerationStrategy?)model[GaussDBAnnotationNames.ValueGenerationStrategy]; + + /// + /// Attempts to set the to use for properties + /// of keys in the model that don't have a strategy explicitly set. + /// + /// The model. + /// The value to set. + public static void SetValueGenerationStrategy(this IMutableModel model, GaussDBValueGenerationStrategy? value) + => model.SetOrRemoveAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy, value); + + /// + /// Attempts to set the to use for properties + /// of keys in the model that don't have a strategy explicitly set. + /// + /// The model. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static GaussDBValueGenerationStrategy? SetValueGenerationStrategy( + this IConventionModel model, + GaussDBValueGenerationStrategy? value, + bool fromDataAnnotation = false) + { + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation); + + return value; + } + + /// + /// Returns the for the default . + /// + /// The model. + /// The for the default . + public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); + + #endregion + + #region PostgreSQL Extensions + + /// + /// 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 GaussDBExtension GetOrAddPostgresExtension( + this IMutableModel model, + string? schema, + string name, + string? version) + => GaussDBExtension.GetOrAddPostgresExtension(model, schema, name, 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. + /// + public static IReadOnlyList GetPostgresExtensions(this IReadOnlyModel model) + => GaussDBExtension.GetPostgresExtensions(model).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 static GaussDBExtension GetOrAddPostgresExtension( + this IConventionModel model, + string? schema, + string name, + string? version) + => GaussDBExtension.GetOrAddPostgresExtension(model, schema, name, version); + + #endregion + + #region Enum types + + /// + /// 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 GaussDBEnum GetOrAddPostgresEnum( + this IMutableModel model, + string? schema, + string name, + string[] labels) + => GaussDBEnum.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 GaussDBEnum GetOrAddPostgresEnum( + this IConventionModel model, + string? schema, + string name, + string[] labels) + => GaussDBEnum.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 IReadOnlyList GetPostgresEnums(this IReadOnlyModel model) + => GaussDBEnum.GetPostgresEnums(model).ToArray(); + + #endregion Enum types + + #region Range types + + /// + /// 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 GaussDBRange GetOrAddPostgresRange( + this IMutableModel model, + string? schema, + string name, + string subtype, + string? canonicalFunction = null, + string? subtypeOpClass = null, + string? collation = null, + string? subtypeDiff = null) + => GaussDBRange.GetOrAddPostgresRange( + model, + schema, + name, + subtype, + canonicalFunction, + subtypeOpClass, + collation, + subtypeDiff); + + /// + /// 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 IReadOnlyList PostgresRanges(this IReadOnlyModel model) + => GaussDBRange.GetPostgresRanges(model).ToArray(); + + #endregion Range types + + #region Database Template + + /// + /// 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? GetDatabaseTemplate(this IReadOnlyModel model) + => (string?)model[GaussDBAnnotationNames.DatabaseTemplate]; + + /// + /// 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 void SetDatabaseTemplate(this IMutableModel model, string? template) + => model.SetOrRemoveAnnotation(GaussDBAnnotationNames.DatabaseTemplate, template); + + /// + /// 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? SetDatabaseTemplate( + this IConventionModel model, + string? template, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(template, nameof(template)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.DatabaseTemplate, template, fromDataAnnotation); + + return template; + } + + /// + /// 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 ConfigurationSource? GetDatabaseTemplateConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.DatabaseTemplate)?.GetConfigurationSource(); + + #endregion + + #region Tablespace + + /// + /// 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? GetTablespace(this IReadOnlyModel model) + => (string?)model[GaussDBAnnotationNames.Tablespace]; + + /// + /// 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 void SetTablespace(this IMutableModel model, string? tablespace) + => model.SetOrRemoveAnnotation(GaussDBAnnotationNames.Tablespace, tablespace); + + /// + /// 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? SetTablespace( + this IConventionModel model, + string? tablespace, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(tablespace, nameof(tablespace)); + + model.SetOrRemoveAnnotation(GaussDBAnnotationNames.Tablespace, tablespace, fromDataAnnotation); + + return tablespace; + } + + /// + /// 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 ConfigurationSource? GetTablespaceConfigurationSource(this IConventionModel model) + => model.FindAnnotation(GaussDBAnnotationNames.Tablespace)?.GetConfigurationSource(); + + #endregion + + #region Collation management + + /// + /// 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 GaussDBCollation GetOrAddCollation( + this IMutableModel model, + string? schema, + string name, + string lcCollate, + string lcCtype, + string? provider = null, + bool? deterministic = null) + => GaussDBCollation.GetOrAddCollation( + model, + schema, + name, + lcCollate, + lcCtype, + provider, + deterministic); + + /// + /// 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 IReadOnlyList GetCollations(this IReadOnlyModel model) + => GaussDBCollation.GetCollations(model).ToArray(); + + #endregion Collation management +} diff --git a/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBPropertyExtensions.cs b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBPropertyExtensions.cs new file mode 100644 index 0000000000..75720d112b --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/MetadataExtensions/GaussDBPropertyExtensions.cs @@ -0,0 +1,1199 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Property extension methods for GaussDB-specific metadata. +/// +/// +/// See Modeling entity types and relationships. +/// +public static class GaussDBPropertyExtensions +{ + #region Hi-lo + + /// + /// Returns the name to use for the hi-lo sequence. + /// + /// The property. + /// The name to use for the hi-lo sequence. + public static string? GetHiLoSequenceName(this IReadOnlyProperty property) + => (string?)property[GaussDBAnnotationNames.HiLoSequenceName]; + + /// + /// Returns the name to use for the hi-lo sequence. + /// + /// The property. + /// The identifier of the store object. + /// The name to use for the hi-lo sequence. + public static string? GetHiLoSequenceName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var annotation = property.FindAnnotation(GaussDBAnnotationNames.HiLoSequenceName); + if (annotation is not null) + { + return (string?)annotation.Value; + } + + var sharedTableRootProperty = property.FindSharedStoreObjectRootProperty(storeObject); + return sharedTableRootProperty is not null + ? sharedTableRootProperty.GetHiLoSequenceName(storeObject) + : null; + } + + /// + /// Sets the name to use for the hi-lo sequence. + /// + /// The property. + /// The sequence name to use. + public static void SetHiLoSequenceName(this IMutableProperty property, string? name) + => property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.HiLoSequenceName, + Check.NullButNotEmpty(name, nameof(name))); + + /// + /// Sets the name to use for the hi-lo sequence. + /// + /// The property. + /// The sequence name to use. + /// Indicates whether the configuration was specified using a data annotation. + public static string? SetHiLoSequenceName( + this IConventionProperty property, + string? name, + bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.HiLoSequenceName, + Check.NullButNotEmpty(name, nameof(name)), + fromDataAnnotation); + + return name; + } + + /// + /// Returns the for the hi-lo sequence name. + /// + /// The property. + /// The for the hi-lo sequence name. + public static ConfigurationSource? GetHiLoSequenceNameConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.HiLoSequenceName)?.GetConfigurationSource(); + + /// + /// Returns the schema to use for the hi-lo sequence. + /// + /// The property. + /// The schema to use for the hi-lo sequence. + public static string? GetHiLoSequenceSchema(this IReadOnlyProperty property) + => (string?)property[GaussDBAnnotationNames.HiLoSequenceSchema]; + + /// + /// Returns the schema to use for the hi-lo sequence. + /// + /// The property. + /// The identifier of the store object. + /// The schema to use for the hi-lo sequence. + public static string? GetHiLoSequenceSchema(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var annotation = property.FindAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema); + if (annotation is not null) + { + return (string?)annotation.Value; + } + + var sharedTableRootProperty = property.FindSharedStoreObjectRootProperty(storeObject); + return sharedTableRootProperty is not null + ? sharedTableRootProperty.GetHiLoSequenceSchema(storeObject) + : null; + } + + /// + /// Sets the schema to use for the hi-lo sequence. + /// + /// The property. + /// The schema to use. + public static void SetHiLoSequenceSchema(this IMutableProperty property, string? schema) + => property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.HiLoSequenceSchema, + Check.NullButNotEmpty(schema, nameof(schema))); + + /// + /// Sets the schema to use for the hi-lo sequence. + /// + /// The property. + /// The schema to use. + /// Indicates whether the configuration was specified using a data annotation. + public static string? SetHiLoSequenceSchema( + this IConventionProperty property, + string? schema, + bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.HiLoSequenceSchema, + Check.NullButNotEmpty(schema, nameof(schema)), + fromDataAnnotation); + + return schema; + } + + /// + /// Returns the for the hi-lo sequence schema. + /// + /// The property. + /// The for the hi-lo sequence schema. + public static ConfigurationSource? GetHiLoSequenceSchemaConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.HiLoSequenceSchema)?.GetConfigurationSource(); + + /// + /// Finds the in the model to use for the hi-lo pattern. + /// + /// The property. + /// The sequence to use, or if no sequence exists in the model. + public static IReadOnlySequence? FindHiLoSequence(this IReadOnlyProperty property) + { + var model = property.DeclaringType.Model; + + var sequenceName = property.GetHiLoSequenceName() + ?? model.GetHiLoSequenceName(); + + var sequenceSchema = property.GetHiLoSequenceSchema() + ?? model.GetHiLoSequenceSchema(); + + return model.FindSequence(sequenceName, sequenceSchema); + } + + /// + /// Finds the in the model to use for the hi-lo pattern. + /// + /// The property. + /// The identifier of the store object. + /// The sequence to use, or if no sequence exists in the model. + public static IReadOnlySequence? FindHiLoSequence(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var model = property.DeclaringType.Model; + + var sequenceName = property.GetHiLoSequenceName(storeObject) + ?? model.GetHiLoSequenceName(); + + var sequenceSchema = property.GetHiLoSequenceSchema(storeObject) + ?? model.GetHiLoSequenceSchema(); + + return model.FindSequence(sequenceName, sequenceSchema); + } + + /// + /// Finds the in the model to use for the hi-lo pattern. + /// + /// The property. + /// The sequence to use, or if no sequence exists in the model. + public static ISequence? FindHiLoSequence(this IProperty property) + => (ISequence?)((IReadOnlyProperty)property).FindHiLoSequence(); + + /// + /// Finds the in the model to use for the hi-lo pattern. + /// + /// The property. + /// The identifier of the store object. + /// The sequence to use, or if no sequence exists in the model. + public static ISequence? FindHiLoSequence(this IProperty property, in StoreObjectIdentifier storeObject) + => (ISequence?)((IReadOnlyProperty)property).FindHiLoSequence(storeObject); + + /// + /// Removes all identity sequence annotations from the property. + /// + public static void RemoveHiLoOptions(this IMutableProperty property) + { + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + } + + /// + /// Removes all identity sequence annotations from the property. + /// + public static void RemoveHiLoOptions(this IConventionProperty property) + { + property.SetHiLoSequenceName(null); + property.SetHiLoSequenceSchema(null); + } + + #endregion Hi-lo + + #region Sequence + + /// + /// Returns the name to use for the key value generation sequence. + /// + /// The property. + /// The name to use for the key value generation sequence. + public static string? GetSequenceName(this IReadOnlyProperty property) + => (string?)property[GaussDBAnnotationNames.SequenceName]; + + /// + /// Returns the name to use for the key value generation sequence. + /// + /// The property. + /// The identifier of the store object. + /// The name to use for the key value generation sequence. + public static string? GetSequenceName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var annotation = property.FindAnnotation(GaussDBAnnotationNames.SequenceName); + if (annotation != null) + { + return (string?)annotation.Value; + } + + return property.FindSharedStoreObjectRootProperty(storeObject)?.GetSequenceName(storeObject); + } + + /// + /// Sets the name to use for the key value generation sequence. + /// + /// The property. + /// The sequence name to use. + public static void SetSequenceName(this IMutableProperty property, string? name) + => property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.SequenceName, + Check.NullButNotEmpty(name, nameof(name))); + + /// + /// Sets the name to use for the key value generation sequence. + /// + /// The property. + /// The sequence name to use. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceName( + this IConventionProperty property, + string? name, + bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.SequenceName, + Check.NullButNotEmpty(name, nameof(name)), + fromDataAnnotation); + + return name; + } + + /// + /// Returns the for the key value generation sequence name. + /// + /// The property. + /// The for the key value generation sequence name. + public static ConfigurationSource? GetSequenceNameConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.SequenceName)?.GetConfigurationSource(); + + /// + /// Returns the schema to use for the key value generation sequence. + /// + /// The property. + /// The schema to use for the key value generation sequence. + public static string? GetSequenceSchema(this IReadOnlyProperty property) + => (string?)property[GaussDBAnnotationNames.SequenceSchema]; + + /// + /// Returns the schema to use for the key value generation sequence. + /// + /// The property. + /// The identifier of the store object. + /// The schema to use for the key value generation sequence. + public static string? GetSequenceSchema(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var annotation = property.FindAnnotation(GaussDBAnnotationNames.SequenceSchema); + if (annotation != null) + { + return (string?)annotation.Value; + } + + return property.FindSharedStoreObjectRootProperty(storeObject)?.GetSequenceSchema(storeObject); + } + + /// + /// Sets the schema to use for the key value generation sequence. + /// + /// The property. + /// The schema to use. + public static void SetSequenceSchema(this IMutableProperty property, string? schema) + => property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.SequenceSchema, + Check.NullButNotEmpty(schema, nameof(schema))); + + /// + /// Sets the schema to use for the key value generation sequence. + /// + /// The property. + /// The schema to use. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetSequenceSchema( + this IConventionProperty property, + string? schema, + bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.SequenceSchema, + Check.NullButNotEmpty(schema, nameof(schema)), + fromDataAnnotation); + + return schema; + } + + /// + /// Returns the for the key value generation sequence schema. + /// + /// The property. + /// The for the key value generation sequence schema. + public static ConfigurationSource? GetSequenceSchemaConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.SequenceSchema)?.GetConfigurationSource(); + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The sequence to use, or if no sequence exists in the model. + public static IReadOnlySequence? FindSequence(this IReadOnlyProperty property) + { + var model = property.DeclaringType.Model; + + var sequenceName = property.GetSequenceName() + ?? model.GetSequenceNameSuffix(); + + var sequenceSchema = property.GetSequenceSchema() + ?? model.GetSequenceSchema(); + + return model.FindSequence(sequenceName, sequenceSchema); + } + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The identifier of the store object. + /// The sequence to use, or if no sequence exists in the model. + public static IReadOnlySequence? FindSequence(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var model = property.DeclaringType.Model; + + var sequenceName = property.GetSequenceName(storeObject) + ?? model.GetSequenceNameSuffix(); + + var sequenceSchema = property.GetSequenceSchema(storeObject) + ?? model.GetSequenceSchema(); + + return model.FindSequence(sequenceName, sequenceSchema); + } + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The sequence to use, or if no sequence exists in the model. + public static ISequence? FindSequence(this IProperty property) + => (ISequence?)((IReadOnlyProperty)property).FindSequence(); + + /// + /// Finds the in the model to use for the key value generation pattern. + /// + /// The property. + /// The identifier of the store object. + /// The sequence to use, or if no sequence exists in the model. + public static ISequence? FindSequence(this IProperty property, in StoreObjectIdentifier storeObject) + => (ISequence?)((IReadOnlyProperty)property).FindSequence(storeObject); + + #endregion Sequence + + #region Value generation + + /// + /// Returns the to use for the property. + /// + /// If no strategy is set for the property, then the strategy to use will be taken from the . + /// + /// + /// The strategy, or if none was set. + public static GaussDBValueGenerationStrategy GetValueGenerationStrategy(this IReadOnlyProperty property) + { + var annotation = property.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy); + if (annotation != null) + { + return (GaussDBValueGenerationStrategy?)annotation.Value ?? GaussDBValueGenerationStrategy.None; + } + + var defaultValueGenerationStrategy = GetDefaultValueGenerationStrategy(property); + if (property.ValueGenerated != ValueGenerated.OnAdd + || property.IsForeignKey() + || property.TryGetDefaultValue(out _) + || (defaultValueGenerationStrategy != GaussDBValueGenerationStrategy.Sequence && property.GetDefaultValueSql() != null) + || property.GetComputedColumnSql() is not null) + { + return GaussDBValueGenerationStrategy.None; + } + + return defaultValueGenerationStrategy; + } + + /// + /// Returns the to use for the property. + /// + /// If no strategy is set for the property, then the strategy to use will be taken from the . + /// + /// + /// The property. + /// The identifier of the store object. + /// The strategy, or if none was set. + public static GaussDBValueGenerationStrategy GetValueGenerationStrategy( + this IReadOnlyProperty property, + in StoreObjectIdentifier storeObject) + => GetValueGenerationStrategy(property, storeObject, null); + + internal static GaussDBValueGenerationStrategy GetValueGenerationStrategy( + this IReadOnlyProperty property, + in StoreObjectIdentifier storeObject, + ITypeMappingSource? typeMappingSource) + { + var @override = property.FindOverrides(storeObject)?.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy); + if (@override != null) + { + return (GaussDBValueGenerationStrategy?)@override.Value ?? GaussDBValueGenerationStrategy.None; + } + + var annotation = property.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy); + if (annotation?.Value != null + && StoreObjectIdentifier.Create(property.DeclaringType, storeObject.StoreObjectType) == storeObject) + { + return (GaussDBValueGenerationStrategy)annotation.Value; + } + + var table = storeObject; + var sharedTableRootProperty = property.FindSharedStoreObjectRootProperty(storeObject); + if (sharedTableRootProperty != null) + { + return sharedTableRootProperty.GetValueGenerationStrategy(storeObject, typeMappingSource) is var npgsqlValueGenerationStrategy + && npgsqlValueGenerationStrategy is + GaussDBValueGenerationStrategy.IdentityByDefaultColumn + or GaussDBValueGenerationStrategy.IdentityAlwaysColumn + or GaussDBValueGenerationStrategy.SerialColumn + && table.StoreObjectType == StoreObjectType.Table + && !property.GetContainingForeignKeys().Any( + fk => + !fk.IsBaseLinking() + || (StoreObjectIdentifier.Create(fk.PrincipalEntityType, StoreObjectType.Table) + is StoreObjectIdentifier principal + && fk.GetConstraintName(table, principal) != null)) + ? npgsqlValueGenerationStrategy + : GaussDBValueGenerationStrategy.None; + } + + if (property.ValueGenerated != ValueGenerated.OnAdd + || table.StoreObjectType != StoreObjectType.Table + || property.TryGetDefaultValue(storeObject, out _) + || property.GetDefaultValueSql(storeObject) != null + || property.GetComputedColumnSql(storeObject) != null + || property.GetContainingForeignKeys() + .Any( + fk => + !fk.IsBaseLinking() + || (StoreObjectIdentifier.Create(fk.PrincipalEntityType, StoreObjectType.Table) + is StoreObjectIdentifier principal + && fk.GetConstraintName(table, principal) != null))) + { + return GaussDBValueGenerationStrategy.None; + } + + var defaultStrategy = GetDefaultValueGenerationStrategy(property, storeObject, typeMappingSource); + if (defaultStrategy != GaussDBValueGenerationStrategy.None) + { + if (annotation != null) + { + return (GaussDBValueGenerationStrategy?)annotation.Value ?? GaussDBValueGenerationStrategy.None; + } + } + + return defaultStrategy; + } + + /// + /// Returns the to use for the property. + /// + /// + /// If no strategy is set for the property, then the strategy to use will be taken from the . + /// + /// The property overrides. + /// The strategy, or if none was set. + public static GaussDBValueGenerationStrategy? GetValueGenerationStrategy( + this IReadOnlyRelationalPropertyOverrides overrides) + => (GaussDBValueGenerationStrategy?)overrides.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy) + ?.Value; + + private static GaussDBValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property) + { + var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); + + switch (modelStrategy) + { + case GaussDBValueGenerationStrategy.SequenceHiLo: + case GaussDBValueGenerationStrategy.SerialColumn: + case GaussDBValueGenerationStrategy.Sequence: + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + return IsCompatibleWithValueGeneration(property) + ? modelStrategy.Value + : GaussDBValueGenerationStrategy.None; + case GaussDBValueGenerationStrategy.None: + case null: + return GaussDBValueGenerationStrategy.None; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private static GaussDBValueGenerationStrategy GetDefaultValueGenerationStrategy( + IReadOnlyProperty property, + in StoreObjectIdentifier storeObject, + ITypeMappingSource? typeMappingSource) + { + var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); + + switch (modelStrategy) + { + case GaussDBValueGenerationStrategy.SequenceHiLo: + return IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource) + ? modelStrategy.Value + : GaussDBValueGenerationStrategy.None; + + case GaussDBValueGenerationStrategy.SerialColumn: + case GaussDBValueGenerationStrategy.Sequence: + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + return !IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource) + ? GaussDBValueGenerationStrategy.None + : property.DeclaringType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy + ? GaussDBValueGenerationStrategy.Sequence + : modelStrategy.Value; + + case GaussDBValueGenerationStrategy.None: + case null: + return GaussDBValueGenerationStrategy.None; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Sets the to use for the property. + /// + /// The property. + /// The strategy to use. + public static void SetValueGenerationStrategy( + this IMutableProperty property, + GaussDBValueGenerationStrategy? value) + => property.SetOrRemoveAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy, value); + + /// + /// Sets the to use for the property. + /// + /// The property. + /// The strategy to use. + /// Indicates whether the configuration was specified using a data annotation. + public static GaussDBValueGenerationStrategy? SetValueGenerationStrategy( + this IConventionProperty property, + GaussDBValueGenerationStrategy? value, + bool fromDataAnnotation = false) + => (GaussDBValueGenerationStrategy?)property.SetOrRemoveAnnotation( + GaussDBAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value; + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property. + /// The strategy to use. + /// The identifier of the table containing the column. + public static void SetValueGenerationStrategy( + this IMutableProperty property, + GaussDBValueGenerationStrategy? value, + in StoreObjectIdentifier storeObject) + => property.GetOrCreateOverrides(storeObject) + .SetValueGenerationStrategy(value); + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property. + /// The strategy to use. + /// The identifier of the table containing the column. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static GaussDBValueGenerationStrategy? SetValueGenerationStrategy( + this IConventionProperty property, + GaussDBValueGenerationStrategy? value, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => property.GetOrCreateOverrides(storeObject, fromDataAnnotation) + .SetValueGenerationStrategy(value, fromDataAnnotation); + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property overrides. + /// The strategy to use. + public static void SetValueGenerationStrategy( + this IMutableRelationalPropertyOverrides overrides, + GaussDBValueGenerationStrategy? value) + => overrides.SetOrRemoveAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy, value); + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property overrides. + /// The strategy to use. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static GaussDBValueGenerationStrategy? SetValueGenerationStrategy( + this IConventionRelationalPropertyOverrides overrides, + GaussDBValueGenerationStrategy? value, + bool fromDataAnnotation = false) + => (GaussDBValueGenerationStrategy?)overrides.SetOrRemoveAnnotation( + GaussDBAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value; + + /// + /// Returns the for the . + /// + /// The property. + /// The for the . + public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); + + /// + /// Returns the for the for a particular table. + /// + /// The property. + /// The identifier of the table containing the column. + /// The for the . + public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( + this IConventionProperty property, + in StoreObjectIdentifier storeObject) + => property.FindOverrides(storeObject)?.GetValueGenerationStrategyConfigurationSource(); + + /// + /// Returns the for the for a particular table. + /// + /// The property overrides. + /// The for the . + public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( + this IConventionRelationalPropertyOverrides overrides) + => overrides.FindAnnotation(GaussDBAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); + + /// + /// Returns a value indicating whether the property is compatible with any . + /// + /// The property. + /// if compatible. + public static bool IsCompatibleWithValueGeneration(IReadOnlyProperty property) + { + var valueConverter = property.GetValueConverter() + ?? property.FindTypeMapping()?.Converter; + + var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); + + return type.IsInteger() || type.IsEnum; + } + + private static bool IsCompatibleWithValueGeneration( + IReadOnlyProperty property, + in StoreObjectIdentifier storeObject, + ITypeMappingSource? typeMappingSource) + { + var valueConverter = property.GetValueConverter() + ?? (property.FindRelationalTypeMapping(storeObject) + ?? typeMappingSource?.FindMapping((IProperty)property))?.Converter; + + var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); + + return type.IsInteger() || type.IsEnum; + } + + #endregion Value generation + + #region Identity sequence options + + /// + /// Returns the identity start value. + /// + /// The property. + /// The identity start value. + public static long? GetIdentityStartValue(this IReadOnlyProperty property) + => IdentitySequenceOptionsData.Get(property).StartValue; + + /// + /// Sets the identity start value. + /// + /// The property. + /// The value to set. + public static void SetIdentityStartValue(this IMutableProperty property, long? startValue) + { + var options = IdentitySequenceOptionsData.Get(property); + options.StartValue = startValue; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize()); + } + + /// + /// Sets the identity start value. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static long? SetIdentityStartValue( + this IConventionProperty property, + long? startValue, + bool fromDataAnnotation = false) + { + var options = IdentitySequenceOptionsData.Get(property); + options.StartValue = startValue; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); + return startValue; + } + + /// + /// Returns the for the identity start value. + /// + /// The property. + /// The for the identity start value. + public static ConfigurationSource? GetIdentityStartValueConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.IdentityOptions)?.GetConfigurationSource(); + + /// + /// Returns the identity increment value. + /// + /// The property. + /// The identity increment value. + public static long? GetIdentityIncrementBy(this IReadOnlyProperty property) + => IdentitySequenceOptionsData.Get(property).IncrementBy; + + /// + /// Sets the identity increment value. + /// + /// The property. + /// The value to set. + public static void SetIdentityIncrementBy(this IMutableProperty property, long? incrementBy) + { + var options = IdentitySequenceOptionsData.Get(property); + options.IncrementBy = incrementBy ?? 1; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize()); + } + + /// + /// Sets the identity increment value. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static long? SetIdentityIncrementBy( + this IConventionProperty property, + long? incrementBy, + bool fromDataAnnotation = false) + { + var options = IdentitySequenceOptionsData.Get(property); + options.IncrementBy = incrementBy ?? 1; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); + return incrementBy; + } + + /// + /// Returns the for the identity increment value. + /// + /// The property. + /// The for the identity increment value. + public static ConfigurationSource? GetIdentityIncrementByConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.IdentityOptions)?.GetConfigurationSource(); + + /// + /// Returns the identity minimum value. + /// + /// The property. + /// The identity minimum value. + public static long? GetIdentityMinValue(this IReadOnlyProperty property) + => IdentitySequenceOptionsData.Get(property).MinValue; + + /// + /// Sets the identity minimum value. + /// + /// The property. + /// The value to set. + public static void SetIdentityMinValue(this IMutableProperty property, long? minValue) + { + var options = IdentitySequenceOptionsData.Get(property); + options.MinValue = minValue; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize()); + } + + /// + /// Sets the identity minimum value. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static long? SetIdentityMinValue( + this IConventionProperty property, + long? minValue, + bool fromDataAnnotation = false) + { + var options = IdentitySequenceOptionsData.Get(property); + options.MinValue = minValue; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); + return minValue; + } + + /// + /// Returns the for the identity minimum value. + /// + /// The property. + /// The for the identity minimum value. + public static ConfigurationSource? GetIdentityMinValueConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.IdentityOptions)?.GetConfigurationSource(); + + /// + /// Returns the identity maximum value. + /// + /// The property. + /// The identity maximum value. + public static long? GetIdentityMaxValue(this IReadOnlyProperty property) + => IdentitySequenceOptionsData.Get(property).MaxValue; + + /// + /// Sets the identity maximum value. + /// + /// The property. + /// The value to set. + public static void SetIdentityMaxValue(this IMutableProperty property, long? maxValue) + { + var options = IdentitySequenceOptionsData.Get(property); + options.MaxValue = maxValue; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize()); + } + + /// + /// Sets the identity maximum value. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static long? SetIdentityMaxValue( + this IConventionProperty property, + long? maxValue, + bool fromDataAnnotation = false) + { + var options = IdentitySequenceOptionsData.Get(property); + options.MaxValue = maxValue; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); + return maxValue; + } + + /// + /// Returns the for the identity maximum value. + /// + /// The property. + /// The for the identity maximum value. + public static ConfigurationSource? GetIdentityMaxValueConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.IdentityOptions)?.GetConfigurationSource(); + + /// + /// Returns whether the identity's sequence is cyclic. + /// + /// The property. + /// Whether the identity's sequence is cyclic. + public static bool? GetIdentityIsCyclic(this IReadOnlyProperty property) + => IdentitySequenceOptionsData.Get(property).IsCyclic; + + /// + /// Sets whether the identity's sequence is cyclic. + /// + /// The property. + /// The value to set. + public static void SetIdentityIsCyclic(this IMutableProperty property, bool? cyclic) + { + var options = IdentitySequenceOptionsData.Get(property); + options.IsCyclic = cyclic ?? false; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize()); + } + + /// + /// Sets whether the identity's sequence is cyclic. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static bool? SetIdentityIsCyclic( + this IConventionProperty property, + bool? cyclic, + bool fromDataAnnotation = false) + { + var options = IdentitySequenceOptionsData.Get(property); + options.IsCyclic = cyclic ?? false; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); + return cyclic; + } + + /// + /// Returns the for whether the identity's sequence is cyclic. + /// + /// The property. + /// The for whether the identity's sequence is cyclic. + public static ConfigurationSource? GetIdentityIsCyclicConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.IdentityOptions)?.GetConfigurationSource(); + + /// + /// Returns the number of sequence numbers to be preallocated and stored in memory for faster access. + /// Defaults to 1 (no cache). + /// + /// The property. + /// The number of sequence numbers to be cached. + public static long? GetIdentityNumbersToCache(this IReadOnlyProperty property) + => IdentitySequenceOptionsData.Get(property).NumbersToCache; + + /// + /// Sets the number of sequence numbers to be preallocated and stored in memory for faster access. + /// + /// The property. + /// The value to set. + public static void SetIdentityNumbersToCache(this IMutableProperty property, long? numbersToCache) + { + var options = IdentitySequenceOptionsData.Get(property); + options.NumbersToCache = numbersToCache ?? 1; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize()); + } + + /// + /// Sets the number of sequence numbers to be preallocated and stored in memory for faster access. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static long? SetIdentityNumbersToCache( + this IConventionProperty property, + long? numbersToCache, + bool fromDataAnnotation = false) + { + var options = IdentitySequenceOptionsData.Get(property); + options.NumbersToCache = numbersToCache ?? 1; + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); + return numbersToCache; + } + + /// + /// Returns the for the number of sequence numbers to be preallocated + /// and stored in memory for faster access. + /// + /// The property. + /// + /// The for the number of sequence numbers to be preallocated + /// and stored in memory for faster access. + /// + public static ConfigurationSource? GetIdentityNumbersToCacheConfigurationSource( + this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.IdentityOptions)?.GetConfigurationSource(); + + /// + /// Removes identity sequence options from the property. + /// + public static void RemoveIdentityOptions(this IMutableProperty property) + => property.RemoveAnnotation(GaussDBAnnotationNames.IdentityOptions); + + /// + /// Removes identity sequence options from the property. + /// + public static void RemoveIdentityOptions(this IConventionProperty property) + => property.RemoveAnnotation(GaussDBAnnotationNames.IdentityOptions); + + #endregion Identity sequence options + + #region Generated tsvector column + + /// + /// Returns the text search configuration for this generated tsvector property, or null if this is not a + /// generated tsvector property. + /// + /// The property. + /// + /// + /// 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. + /// + /// + public static string? GetTsVectorConfig(this IReadOnlyProperty property) + => (string?)property[GaussDBAnnotationNames.TsVectorConfig]; + + /// + /// Sets the text search configuration for this generated tsvector property, or null if this is not a + /// generated tsvector property. + /// + /// The property. + /// + /// + /// 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. + /// + /// + public static void SetTsVectorConfig(this IMutableProperty property, string? config) + { + Check.NullButNotEmpty(config, nameof(config)); + + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.TsVectorConfig, config); + } + + /// + /// Returns the text search configuration for this generated tsvector property, or null if this is not a + /// generated tsvector property. + /// + /// The property. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// + /// 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. + /// + /// + public static string SetTsVectorConfig( + this IConventionProperty property, + string config, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(config, nameof(config)); + + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.TsVectorConfig, config, fromDataAnnotation); + + return config; + } + + /// + /// Returns the for the text search configuration for the generated tsvector + /// property. + /// + /// The property. + /// The configuration source for the text search configuration for the generated tsvector property. + public static ConfigurationSource? GetTsVectorConfigConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.TsVectorConfig)?.GetConfigurationSource(); + + /// + /// Returns the properties included in this generated tsvector property, or null if this is not a + /// generated tsvector property. + /// + /// The property. + /// The included property names, or null if this is not a Generated tsvector column. + public static IReadOnlyList? GetTsVectorProperties(this IReadOnlyProperty property) + => (string[]?)property[GaussDBAnnotationNames.TsVectorProperties]; + + /// + /// Sets the properties included in this generated tsvector property, or null to make this a regular, + /// non-generated property. + /// + /// The property. + /// The included property names. + public static void SetTsVectorProperties( + this IMutableProperty property, + IReadOnlyList? properties) + { + Check.NullButNotEmpty(properties, nameof(properties)); + + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.TsVectorProperties, properties); + } + + /// + /// Sets properties included in this generated tsvector property, or null to make this a regular, + /// non-generated property. + /// + /// The property. + /// Indicates whether the configuration was specified using a data annotation. + /// The included property names. + public static IReadOnlyList? SetTsVectorProperties( + this IConventionProperty property, + IReadOnlyList? properties, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(properties, nameof(properties)); + + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.TsVectorProperties, properties, fromDataAnnotation); + + return properties; + } + + /// + /// Returns the for the properties included in the generated tsvector property. + /// + /// The property. + /// The configuration source for the properties included in the generated tsvector property. + public static ConfigurationSource? GetTsVectorPropertiesConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(GaussDBAnnotationNames.TsVectorConfig)?.GetConfigurationSource(); + + #endregion Generated tsvector column + + #region Compression method + + /// + /// Returns the compression method to be used, or null if it hasn't been specified. + /// + /// This feature was introduced in GaussDB 14. + public static string? GetCompressionMethod(this IReadOnlyProperty property) + => (property is RuntimeProperty) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (string?)property[GaussDBAnnotationNames.CompressionMethod]; + + /// + /// Returns the compression method to be used, or null if it hasn't been specified. + /// + /// This feature was introduced in GaussDB 14. + public static string? GetCompressionMethod(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property is RuntimeProperty + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : property.FindAnnotation(GaussDBAnnotationNames.CompressionMethod) is { } annotation + ? (string?)annotation.Value + : property.FindSharedStoreObjectRootProperty(storeObject)?.GetCompressionMethod(storeObject); + + /// + /// Sets the compression method to be used, or null if it hasn't been specified. + /// + /// This feature was introduced in GaussDB 14. + public static void SetCompressionMethod(this IMutableProperty property, string? compressionMethod) + => property.SetOrRemoveAnnotation(GaussDBAnnotationNames.CompressionMethod, compressionMethod); + + /// + /// Sets the compression method to be used, or null if it hasn't been specified. + /// + /// This feature was introduced in GaussDB 14. + public static string? SetCompressionMethod( + this IConventionProperty property, + string? compressionMethod, + bool fromDataAnnotation = false) + { + Check.NullButNotEmpty(compressionMethod, nameof(compressionMethod)); + + property.SetOrRemoveAnnotation(GaussDBAnnotationNames.CompressionMethod, compressionMethod, fromDataAnnotation); + + return compressionMethod; + } + + /// + /// Returns the for the compression method. + /// + /// The property. + /// The for the compression method. + public static ConfigurationSource? GetCompressionMethodConfigurationSource(this IConventionProperty index) + => index.FindAnnotation(GaussDBAnnotationNames.IndexMethod)?.GetConfigurationSource(); + + #endregion Compression method +} diff --git a/src/EFCore.PG/Extensions/MethodInfoExtensions.cs b/src/EFCore.GaussDB/Extensions/MethodInfoExtensions.cs similarity index 100% rename from src/EFCore.PG/Extensions/MethodInfoExtensions.cs rename to src/EFCore.GaussDB/Extensions/MethodInfoExtensions.cs diff --git a/src/EFCore.PG/Extensions/PropertyInfoExtensions.cs b/src/EFCore.GaussDB/Extensions/PropertyInfoExtensions.cs similarity index 100% rename from src/EFCore.PG/Extensions/PropertyInfoExtensions.cs rename to src/EFCore.GaussDB/Extensions/PropertyInfoExtensions.cs diff --git a/src/EFCore.GaussDB/Extensions/RelationalTypeMappingExtensions.cs b/src/EFCore.GaussDB/Extensions/RelationalTypeMappingExtensions.cs new file mode 100644 index 0000000000..61a05e315c --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/RelationalTypeMappingExtensions.cs @@ -0,0 +1,12 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Storage; + +internal static class RelationalTypeMappingExtensions +{ + internal static string GenerateEmbeddedSqlLiteral(this RelationalTypeMapping mapping, object? value) + => mapping is GaussDBTypeMapping npgsqlTypeMapping + ? npgsqlTypeMapping.GenerateEmbeddedSqlLiteral(value) + : mapping.GenerateSqlLiteral(value); +} diff --git a/src/EFCore.GaussDB/Extensions/TypeExtensions.cs b/src/EFCore.GaussDB/Extensions/TypeExtensions.cs new file mode 100644 index 0000000000..fd430ed290 --- /dev/null +++ b/src/EFCore.GaussDB/Extensions/TypeExtensions.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +// ReSharper disable once CheckNamespace +namespace System.Reflection; + +internal static class TypeExtensions +{ + internal static bool IsGenericList(this Type? type) + => type is not null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + + internal static bool IsArrayOrGenericList(this Type type) + => type.IsArray || type.IsGenericList(); + + internal static bool TryGetElementType(this Type type, [NotNullWhen(true)] out Type? elementType) + { + elementType = type.IsArray + ? type.GetElementType() + : type.IsGenericList() + ? type.GetGenericArguments()[0] + : null; + return elementType is not null; + } + + internal static bool IsRange(this SqlExpression expression) + => expression.TypeMapping is GaussDBCidrTypeMapping || expression.Type.IsRange(); + + internal static bool IsRange(this Type type) + => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(GaussDBRange<>) + || type is { Name: "Interval" or "DateInterval", Namespace: "NodaTime" }; + + internal static bool IsMultirange(this SqlExpression expression) + => expression.TypeMapping is GaussDBMultirangeTypeMapping + || expression.Type.IsMultirange(); + + internal static bool IsMultirange(this Type type) + => type.TryGetElementType(out var elementType) && elementType.IsRange(); + + public static PropertyInfo? FindIndexerProperty(this Type type) + { + var defaultPropertyAttribute = type.GetCustomAttributes().FirstOrDefault(); + + return defaultPropertyAttribute is null + ? null + : type.GetRuntimeProperties() + .FirstOrDefault(pi => pi.Name == defaultPropertyAttribute.MemberName && pi.GetIndexParameters().Length == 1); + } +} diff --git a/src/EFCore.PG/Extensions/VersionExtensions.cs b/src/EFCore.GaussDB/Extensions/VersionExtensions.cs similarity index 100% rename from src/EFCore.PG/Extensions/VersionExtensions.cs rename to src/EFCore.GaussDB/Extensions/VersionExtensions.cs diff --git a/src/EFCore.GaussDB/GaussDBRetryingExecutionStrategy.cs b/src/EFCore.GaussDB/GaussDBRetryingExecutionStrategy.cs new file mode 100644 index 0000000000..3b1d579d50 --- /dev/null +++ b/src/EFCore.GaussDB/GaussDBRetryingExecutionStrategy.cs @@ -0,0 +1,126 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +/// +/// An implementation for retrying failed executions on GaussDB. +/// +/// +/// +/// The service lifetime is . This means that each instance will use +/// its own instance of this service. The implementation may depend on other services registered with any lifetime. The +/// implementation does not need to be thread-safe. +/// +/// +/// See Connection resiliency and database retries for more +/// information and examples. +/// +/// +public class GaussDBRetryingExecutionStrategy : ExecutionStrategy +{ + private readonly ICollection? _additionalErrorCodes; + + /// + /// Creates a new instance of . + /// + /// The context on which the operations will be invoked. + /// + /// The default retry limit is 6, which means that the total amount of time spent before failing is about a minute. + /// + public GaussDBRetryingExecutionStrategy( + DbContext context) + : this(context, DefaultMaxRetryCount) + { + } + + /// + /// Creates a new instance of . + /// + /// Parameter object containing service dependencies. + public GaussDBRetryingExecutionStrategy( + ExecutionStrategyDependencies dependencies) + : this(dependencies, DefaultMaxRetryCount) + { + } + + /// + /// Creates a new instance of . + /// + /// The context on which the operations will be invoked. + /// The maximum number of retry attempts. + public GaussDBRetryingExecutionStrategy( + DbContext context, + int maxRetryCount) + : this(context, maxRetryCount, DefaultMaxDelay, errorCodesToAdd: null) + { + } + + /// + /// Creates a new instance of . + /// + /// Parameter object containing service dependencies. + /// The maximum number of retry attempts. + public GaussDBRetryingExecutionStrategy( + ExecutionStrategyDependencies dependencies, + int maxRetryCount) + : this(dependencies, maxRetryCount, DefaultMaxDelay, errorCodesToAdd: null) + { + } + + /// + /// Creates a new instance of . + /// + /// Parameter object containing service dependencies. + /// Additional error codes that should be considered transient. + public GaussDBRetryingExecutionStrategy( + ExecutionStrategyDependencies dependencies, + ICollection? errorCodesToAdd) + : this(dependencies, DefaultMaxRetryCount, DefaultMaxDelay, errorCodesToAdd) + { + } + + /// + /// Creates a new instance of . + /// + /// The context on which the operations will be invoked. + /// The maximum number of retry attempts. + /// The maximum delay between retries. + /// Additional error codes that should be considered transient. + public GaussDBRetryingExecutionStrategy( + DbContext context, + int maxRetryCount, + TimeSpan maxRetryDelay, + ICollection? errorCodesToAdd) + : base( + context, + maxRetryCount, + maxRetryDelay) + { + _additionalErrorCodes = errorCodesToAdd; + } + + /// + /// Creates a new instance of . + /// + /// Parameter object containing service dependencies. + /// The maximum number of retry attempts. + /// The maximum delay between retries. + /// Additional SQL error numbers that should be considered transient. + public GaussDBRetryingExecutionStrategy( + ExecutionStrategyDependencies dependencies, + int maxRetryCount, + TimeSpan maxRetryDelay, + ICollection? errorCodesToAdd) + : base(dependencies, maxRetryCount, maxRetryDelay) + { + _additionalErrorCodes = errorCodesToAdd; + } + + // TODO: Unlike SqlException, which seems to also wrap various transport/IO errors + // and expose them via error codes, we have GaussDBException with an inner exception. + // Would be good to provide a way to add these into the additional list. + /// + protected override bool ShouldRetryOn(Exception? exception) + => exception is PostgresException postgresException && _additionalErrorCodes?.Contains(postgresException.SqlState) == true + || GaussDBTransientExceptionDetector.ShouldRetryOn(exception); +} diff --git a/src/EFCore.GaussDB/Infrastructure/EntityFrameworkGaussDBServicesBuilder.cs b/src/EFCore.GaussDB/Infrastructure/EntityFrameworkGaussDBServicesBuilder.cs new file mode 100644 index 0000000000..160a363907 --- /dev/null +++ b/src/EFCore.GaussDB/Infrastructure/EntityFrameworkGaussDBServicesBuilder.cs @@ -0,0 +1,36 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +/// +/// A builder API designed for GaussDB when registering services. +/// +public class EntityFrameworkGaussDBServicesBuilder : EntityFrameworkRelationalServicesBuilder +{ + private static readonly IDictionary GaussDBServices + = new Dictionary + { + { + typeof(IGaussDBDataSourceConfigurationPlugin), + 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 EntityFrameworkGaussDBServicesBuilder(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) + => GaussDBServices.TryGetValue(serviceType, out var characteristics) + ? characteristics + : base.TryGetServiceCharacteristics(serviceType); +} diff --git a/src/EFCore.GaussDB/Infrastructure/GaussDBDbContextOptionsBuilder.cs b/src/EFCore.GaussDB/Infrastructure/GaussDBDbContextOptionsBuilder.cs new file mode 100644 index 0000000000..ad34a1642d --- /dev/null +++ b/src/EFCore.GaussDB/Infrastructure/GaussDBDbContextOptionsBuilder.cs @@ -0,0 +1,232 @@ +using System.Net.Security; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDBTypes; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +/// +/// Allows for options specific to GaussDB to be configured for a . +/// +public class GaussDBDbContextOptionsBuilder + : RelationalDbContextOptionsBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The core options builder. + public GaussDBDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) + : base(optionsBuilder) + { + } + + /// + /// Configures lower-level GaussDB options at the ADO.NET driver level. + /// + /// A lambda to configure GaussDB 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 GaussDBDbContextOptionsBuilder ConfigureDataSource(Action dataSourceBuilderAction) + => WithOption(e => e.WithDataSourceConfiguration(dataSourceBuilderAction)); + + /// + /// Connect to this database for administrative operations (creating/dropping databases). + /// + /// The name of the database for administrative operations. + public virtual GaussDBDbContextOptionsBuilder UseAdminDatabase(string? dbName) + => WithOption(e => e.WithAdminDatabase(dbName)); + + /// + /// Configures the backend version to target. + /// + /// The backend version to target. + public virtual GaussDBDbContextOptionsBuilder SetPostgresVersion(Version? postgresVersion) + => WithOption(e => e.WithPostgresVersion(postgresVersion)); + + /// + /// Configures the backend version to target. + /// + /// The GaussDB major version to target. + /// The GaussDB minor version to target. + public virtual GaussDBDbContextOptionsBuilder SetPostgresVersion(int major, int minor) + => SetPostgresVersion(new Version(major, minor)); + + /// + /// Configures the provider to work in Redshift compatibility mode, which avoids certain unsupported features from modern + /// GaussDB versions. + /// + /// Whether to target Redshift. + public virtual GaussDBDbContextOptionsBuilder UseRedshift(bool useRedshift = true) + => WithOption(e => e.WithRedshift(useRedshift)); + + #region MapRange + + /// + /// Maps a user-defined GaussDB range type for use. + /// + /// + /// The CLR type of the range's subtype (or element). + /// The actual mapped type will be an over this type. + /// + /// The name of the GaussDB range type to be mapped. + /// The name of the GaussDB schema in which the range is defined. + /// + /// Optionally, the name of the range's GaussDB subtype (or element). + /// This is usually not needed - the subtype will be inferred based on . + /// + /// + /// To map a range of GaussDB real, use the following: + /// GaussDBTypeMappingSource.MapRange{float}("floatrange"); + /// + public virtual GaussDBDbContextOptionsBuilder MapRange( + string rangeName, + string? schemaName = null, + string? subtypeName = null) + => MapRange(rangeName, typeof(TSubtype), schemaName, subtypeName); + + /// + /// Maps a user-defined GaussDB range type for use. + /// + /// The name of the GaussDB range type to be mapped. + /// The name of the GaussDB schema in which the range is defined. + /// + /// The CLR type of the range's subtype (or element). + /// The actual mapped type will be an over this type. + /// + /// + /// Optionally, the name of the range's GaussDB subtype (or element). + /// This is usually not needed - the subtype will be inferred based on . + /// + /// + /// To map a range of GaussDB real, use the following: + /// GaussDBTypeMappingSource.MapRange("floatrange", typeof(float)); + /// + public virtual GaussDBDbContextOptionsBuilder MapRange( + string rangeName, + Type subtypeClrType, + string? schemaName = null, + string? subtypeName = null) + => WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName)); + + #endregion MapRange + + #region MapEnum + + /// + /// Maps a GaussDB enum type for use. + /// + /// The name of the GaussDB enum type to be mapped. + /// The name of the GaussDB schema in which the range is defined. + /// The name translator used to map enum value names to GaussDB enum values. + public virtual GaussDBDbContextOptionsBuilder MapEnum( + string? enumName = null, + string? schemaName = null, + IGaussDBNameTranslator? nameTranslator = null) + where T : struct, Enum + => MapEnum(typeof(T), enumName, schemaName, nameTranslator); + + /// + /// Maps a GaussDB enum type for use. + /// + /// The CLR type of the enum. + /// The name of the GaussDB enum type to be mapped. + /// The name of the GaussDB schema in which the range is defined. + /// The name translator used to map enum value names to GaussDB enum values. + public virtual GaussDBDbContextOptionsBuilder MapEnum( + Type clrType, + string? enumName = null, + string? schemaName = null, + IGaussDBNameTranslator? 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 + /// accordingly, and since this isn't done this feature isn't publicly exposed. + /// + /// True to enable reverse null ordering; otherwise, false. + internal virtual GaussDBDbContextOptionsBuilder ReverseNullOrdering(bool reverseNullOrdering = true) + => WithOption(e => e.WithReverseNullOrdering(reverseNullOrdering)); + + #region Authentication (obsolete) + + /// + /// Configures the to use the specified . + /// + /// The callback to use. + [Obsolete("Call ConfigureDataSource() and configure the client certificates on the GaussDBDataSourceBuilder, or pass an externally-built, pre-configured GaussDBDataSource to UseGaussDB().")] + public virtual GaussDBDbContextOptionsBuilder ProvideClientCertificatesCallback(ProvideClientCertificatesCallback? callback) + => WithOption(e => e.WithProvideClientCertificatesCallback(callback)); + + /// + /// Configures the to use the specified . + /// + /// The callback to use. + [Obsolete("Call ConfigureDataSource() and configure remote certificate validation on the GaussDBDataSourceBuilder, or pass an externally-built, pre-configured GaussDBDataSource to UseGaussDB().")] + public virtual GaussDBDbContextOptionsBuilder RemoteCertificateValidationCallback(RemoteCertificateValidationCallback? callback) + => WithOption(e => e.WithRemoteCertificateValidationCallback(callback)); + + /// + /// Configures the to use the specified . + /// + /// The callback to use. + [Obsolete("Call ConfigureDataSource() and configure the password callback on the GaussDBDataSourceBuilder, or pass an externally-built, pre-configured GaussDBDataSource to UseGaussDB().")] + public virtual GaussDBDbContextOptionsBuilder ProvidePasswordCallback(ProvidePasswordCallback? callback) + => WithOption(e => e.WithProvidePasswordCallback(callback)); + + #endregion Authentication (obsolete) + + #region Retrying execution strategy + + /// + /// Configures the context to use the default retrying . + /// + /// + /// An instance of configured to use + /// the default retrying . + /// + public virtual GaussDBDbContextOptionsBuilder EnableRetryOnFailure() + => ExecutionStrategy(c => new GaussDBRetryingExecutionStrategy(c)); + + /// + /// Configures the context to use the default retrying . + /// + /// + /// An instance of with the specified parameters. + /// + public virtual GaussDBDbContextOptionsBuilder EnableRetryOnFailure(int maxRetryCount) + => ExecutionStrategy(c => new GaussDBRetryingExecutionStrategy(c, maxRetryCount)); + + /// + /// Configures the context to use the default retrying . + /// + /// Additional error codes that should be considered transient. + /// + /// An instance of with the specified parameters. + /// + public virtual GaussDBDbContextOptionsBuilder EnableRetryOnFailure(ICollection? errorCodesToAdd) + => ExecutionStrategy(c => new GaussDBRetryingExecutionStrategy(c, errorCodesToAdd)); + + /// + /// Configures the context to use the default retrying . + /// + /// The maximum number of retry attempts. + /// The maximum delay between retries. + /// Additional error codes that should be considered transient. + /// + /// An instance of with the specified parameters. + /// + public virtual GaussDBDbContextOptionsBuilder EnableRetryOnFailure( + int maxRetryCount, + TimeSpan maxRetryDelay, + ICollection? errorCodesToAdd) + => ExecutionStrategy(c => new GaussDBRetryingExecutionStrategy(c, maxRetryCount, maxRetryDelay, errorCodesToAdd)); + + #endregion Retrying execution strategy +} diff --git a/src/EFCore.GaussDB/Infrastructure/IGaussDBDataSourceConfigurationPlugin.cs b/src/EFCore.GaussDB/Infrastructure/IGaussDBDataSourceConfigurationPlugin.cs new file mode 100644 index 0000000000..a02c2e882d --- /dev/null +++ b/src/EFCore.GaussDB/Infrastructure/IGaussDBDataSourceConfigurationPlugin.cs @@ -0,0 +1,26 @@ +using HuaweiCloud.GaussDB; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 IGaussDBDataSourceConfigurationPlugin +{ + /// + /// Applies the plugin configuration on the given . + /// + void Configure(GaussDBDataSourceBuilder gaussdbDataSourceBuilder); +} diff --git a/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs b/src/EFCore.GaussDB/Infrastructure/Internal/EnumDefinition.cs similarity index 92% rename from src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs rename to src/EFCore.GaussDB/Infrastructure/Internal/EnumDefinition.cs index b02ad9b781..8b35f74904 100644 --- a/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs +++ b/src/EFCore.GaussDB/Infrastructure/Internal/EnumDefinition.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDBTypes; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -11,7 +13,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; public sealed class EnumDefinition : IEquatable { /// - /// Maps the CLR member values to the PostgreSQL value labels. + /// Maps the CLR member values to the GaussDB value labels. /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -22,7 +24,7 @@ public sealed class EnumDefinition : IEquatable public IReadOnlyDictionary Labels { get; } /// - /// The name of the PostgreSQL enum type to be mapped. + /// The name of the GaussDB enum type to be mapped. /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,7 +35,7 @@ public sealed class EnumDefinition : IEquatable public string StoreTypeName { get; } /// - /// The PostgreSQL schema in which the enum is defined. If null, the default schema is used + /// The GaussDB schema in which the enum is defined. If null, the default schema is used /// (which is public unless changed on the model). /// /// @@ -61,7 +63,7 @@ public sealed class EnumDefinition : IEquatable /// 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; } + public IGaussDBNameTranslator NameTranslator { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -73,7 +75,7 @@ public EnumDefinition( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type clrType, string? name, string? schema, - INpgsqlNameTranslator nameTranslator) + IGaussDBNameTranslator nameTranslator) { if (clrType is not { IsEnum: true, IsClass: false }) { diff --git a/src/EFCore.GaussDB/Infrastructure/Internal/GaussDBModelValidator.cs b/src/EFCore.GaussDB/Infrastructure/Internal/GaussDBModelValidator.cs new file mode 100644 index 0000000000..824c3fb4ba --- /dev/null +++ b/src/EFCore.GaussDB/Infrastructure/Internal/GaussDBModelValidator.cs @@ -0,0 +1,271 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBModelValidator : RelationalModelValidator +{ + /// + /// The backend version to target. + /// + private readonly Version _postgresVersion; + + /// + /// 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 GaussDBModelValidator( + ModelValidatorDependencies dependencies, + RelationalModelValidatorDependencies relationalDependencies, + IGaussDBSingletonOptions npgsqlSingletonOptions) + : base(dependencies, relationalDependencies) + { + _postgresVersion = npgsqlSingletonOptions.PostgresVersion; + } + + /// + /// 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 Validate(IModel model, IDiagnosticsLogger logger) + { + base.Validate(model, logger); + + ValidateIdentityVersionCompatibility(model); + ValidateIndexIncludeProperties(model); + } + + /// + /// Validates that identity columns are used only with GaussDB 10.0 or later. + /// + /// The model to validate. + protected virtual void ValidateIdentityVersionCompatibility(IModel model) + { + if (_postgresVersion.AtLeast(10)) + { + return; + } + + var strategy = model.GetValueGenerationStrategy(); + + if (strategy is GaussDBValueGenerationStrategy.IdentityAlwaysColumn or GaussDBValueGenerationStrategy.IdentityByDefaultColumn) + { + throw new InvalidOperationException( + $"'{strategy}' requires GaussDB 10.0 or later. " + + "If you're using an older version, set GaussDB compatibility mode by calling " + + $"'optionsBuilder.{nameof(GaussDBDbContextOptionsBuilder.SetPostgresVersion)}()' in your model's OnConfiguring. " + + "See the docs for more info."); + } + + foreach (var property in model.GetEntityTypes().SelectMany(e => e.GetProperties())) + { + var propertyStrategy = property.GetValueGenerationStrategy(); + + if (propertyStrategy is GaussDBValueGenerationStrategy.IdentityAlwaysColumn + or GaussDBValueGenerationStrategy.IdentityByDefaultColumn) + { + throw new InvalidOperationException( + $"{property.DeclaringType}.{property.Name}: '{propertyStrategy}' requires GaussDB 10.0 or later."); + } + } + } + + /// + /// 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 void ValidateValueGeneration( + IEntityType entityType, + IKey key, + IDiagnosticsLogger logger) + { + if (entityType.GetTableName() != null + && (string?)entityType[RelationalAnnotationNames.MappingStrategy] == RelationalAnnotationNames.TpcMappingStrategy) + { + foreach (var storeGeneratedProperty in key.Properties.Where( + p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0 + && p.GetValueGenerationStrategy() != GaussDBValueGenerationStrategy.Sequence)) + { + logger.TpcStoreGeneratedIdentityWarning(storeGeneratedProperty); + } + } + } + + /// + 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 GaussDBValueGenerationStrategy.None: + break; + + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + if (!GaussDBPropertyExtensions.IsCompatibleWithValueGeneration(property)) + { + throw new InvalidOperationException( + GaussDBStrings.IdentityBadType( + property.Name, property.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); + } + + break; + + case GaussDBValueGenerationStrategy.SequenceHiLo: + case GaussDBValueGenerationStrategy.Sequence: + case GaussDBValueGenerationStrategy.SerialColumn: + if (!GaussDBPropertyExtensions.IsCompatibleWithValueGeneration(property)) + { + throw new InvalidOperationException( + GaussDBStrings.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 + /// 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 void ValidateIndexIncludeProperties(IModel model) + { + foreach (var index in model.GetEntityTypes().SelectMany(t => t.GetDeclaredIndexes())) + { + var includeProperties = index.GetIncludeProperties(); + if (includeProperties?.Count > 0) + { + var notFound = includeProperties + .FirstOrDefault(i => index.DeclaringEntityType.FindProperty(i) is null); + + if (notFound is not null) + { + throw new InvalidOperationException( + GaussDBStrings.IncludePropertyNotFound(index.DeclaringEntityType.DisplayName(), notFound)); + } + + var duplicate = includeProperties + .GroupBy(i => i) + .Where(g => g.Count() > 1) + .Select(y => y.Key) + .FirstOrDefault(); + + if (duplicate is not null) + { + throw new InvalidOperationException( + GaussDBStrings.IncludePropertyDuplicated(index.DeclaringEntityType.DisplayName(), duplicate)); + } + + var inIndex = includeProperties + .FirstOrDefault(i => index.Properties.Any(p => i == p.Name)); + + if (inIndex is not null) + { + throw new InvalidOperationException( + GaussDBStrings.IncludePropertyInIndex(index.DeclaringEntityType.DisplayName(), inIndex)); + } + } + } + } + + /// + protected override void ValidateStoredProcedures( + IModel model, + IDiagnosticsLogger logger) + { + base.ValidateStoredProcedures(model, logger); + + foreach (var entityType in model.GetEntityTypes()) + { + if (entityType.GetDeleteStoredProcedure() is { } deleteStoredProcedure) + { + ValidateSproc(deleteStoredProcedure, logger); + } + + if (entityType.GetInsertStoredProcedure() is { } insertStoredProcedure) + { + ValidateSproc(insertStoredProcedure, logger); + } + + if (entityType.GetUpdateStoredProcedure() is { } updateStoredProcedure) + { + ValidateSproc(updateStoredProcedure, logger); + } + } + + static void ValidateSproc(IStoredProcedure sproc, IDiagnosticsLogger logger) + { + var entityType = sproc.EntityType; + var storeObjectIdentifier = sproc.GetStoreIdentifier(); + + if (sproc.ResultColumns.Any()) + { + throw new InvalidOperationException( + GaussDBStrings.StoredProcedureResultColumnsNotSupported( + entityType.DisplayName(), + storeObjectIdentifier.DisplayName())); + } + + if (sproc.IsRowsAffectedReturned) + { + throw new InvalidOperationException( + GaussDBStrings.StoredProcedureReturnValueNotSupported( + entityType.DisplayName(), + storeObjectIdentifier.DisplayName())); + } + } + } + + /// + protected override void ValidateCompatible( + IProperty property, + IProperty duplicateProperty, + string columnName, + in StoreObjectIdentifier storeObject, + IDiagnosticsLogger logger) + { + base.ValidateCompatible(property, duplicateProperty, columnName, storeObject, logger); + + if (property.GetCompressionMethod(storeObject) != duplicateProperty.GetCompressionMethod(storeObject)) + { + throw new InvalidOperationException( + GaussDBStrings.DuplicateColumnCompressionMethodMismatch( + duplicateProperty.DeclaringType.DisplayName(), + duplicateProperty.Name, + property.DeclaringType.DisplayName(), + property.Name, + columnName, + storeObject.DisplayName())); + } + } +} diff --git a/src/EFCore.GaussDB/Infrastructure/Internal/GaussDBOptionsExtension.cs b/src/EFCore.GaussDB/Infrastructure/Internal/GaussDBOptionsExtension.cs new file mode 100644 index 0000000000..445886c149 --- /dev/null +++ b/src/EFCore.GaussDB/Infrastructure/Internal/GaussDBOptionsExtension.cs @@ -0,0 +1,583 @@ +using System.Data.Common; +using System.Globalization; +using System.Net.Security; +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +/// +/// Represents options managed by the GaussDB. +/// +public class GaussDBOptionsExtension : RelationalOptionsExtension +{ + private DbContextOptionsExtensionInfo? _info; + //private ParameterizedCollectionMode? _parameterizedCollectionMode; + + private readonly List _userRangeDefinitions; + private readonly List _enumDefinitions; + private Version? _postgresVersion; + + // We override ParameterizedCollectionMode to set Parameter as the default instead of MultipleParameters, + // which is the EF relational default. In GaussDB using native array parameters is better, and the + // query plan problem can be mitigated by telling GaussDB to use a custom plan (see #3269). + + ///// + ///// 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 ParameterizedCollectionMode ParameterizedCollectionMode + // => _parameterizedCollectionMode ?? ParameterizedCollectionMode.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. + /// + public static readonly Version DefaultPostgresVersion = new(14, 0); + + /// + /// The backend version to target. + /// + public virtual Version PostgresVersion + => _postgresVersion ?? DefaultPostgresVersion; + + /// + /// The backend version to target, but returns unless the user explicitly specified a version. + /// + public virtual bool IsPostgresVersionSet + => _postgresVersion is not null; + + /// + /// A lambda to configure GaussDB options on . + /// + public virtual Action? DataSourceBuilderAction { get; private set; } + + /// + /// The , or if a connection string or was used + /// instead of a . + /// + public virtual DbDataSource? DataSource { get; private set; } + + /// + /// The name of the database for administrative operations. + /// + public virtual string? AdminDatabase { get; private set; } + + /// + /// Whether to target Redshift. + /// + public virtual bool UseRedshift { get; private set; } + + /// + /// The list of range mappings specified by the user. + /// + public virtual IReadOnlyList UserRangeDefinitions + => _userRangeDefinitions; + + /// + /// The list of range mappings specified by the user. + /// + public virtual IReadOnlyList EnumDefinitions + => _enumDefinitions; + + /// + /// The specified . + /// + public virtual ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; private set; } + + /// + /// The specified . + /// + public virtual RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; private set; } + + /// + /// The specified . + /// +#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete + public virtual ProvidePasswordCallback? ProvidePasswordCallback { get; private set; } +#pragma warning restore CS0618 + + /// + /// True if reverse null ordering is enabled; otherwise, false. + /// + public virtual bool ReverseNullOrdering { get; private set; } + + /// + /// Initializes an instance of with the default settings. + /// + public GaussDBOptionsExtension() + { + _userRangeDefinitions = []; + _enumDefinitions = []; + } + + // NB: When adding new options, make sure to update the copy ctor below. + /// + /// Initializes an instance of by copying the specified instance. + /// + /// The instance to copy. + public GaussDBOptionsExtension(GaussDBOptionsExtension copyFrom) + : base(copyFrom) + { + DataSource = copyFrom.DataSource; + DataSourceBuilderAction = copyFrom.DataSourceBuilderAction; + AdminDatabase = copyFrom.AdminDatabase; + _postgresVersion = copyFrom._postgresVersion; + UseRedshift = copyFrom.UseRedshift; + _userRangeDefinitions = [..copyFrom._userRangeDefinitions]; + _enumDefinitions = [..copyFrom._enumDefinitions]; + ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback; + RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback; + ProvidePasswordCallback = copyFrom.ProvidePasswordCallback; + ReverseNullOrdering = copyFrom.ReverseNullOrdering; + } + + // The following is a hack to set the default minimum batch size to 2 in GaussDB + // See https://github.com/aspnet/EntityFrameworkCore/pull/10091 + /// + /// 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 int? MinBatchSize + => base.MinBatchSize ?? 2; + + // We need to override WithUseParameterizedCollectionMode since we override ParameterizedCollectionMode above + ///// + //public override RelationalOptionsExtension WithUseParameterizedCollectionMode(ParameterizedCollectionMode parameterizedCollectionMode) + //{ + // var clone = (GaussDBOptionsExtension)Clone(); + + // clone._parameterizedCollectionMode = parameterizedCollectionMode; + + // 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 . + /// + /// A lambda to configure GaussDB options on . + /// A new instance with the option changed. + public virtual GaussDBOptionsExtension WithDataSourceConfiguration(Action dataSourceBuilderAction) + { + var clone = (GaussDBOptionsExtension)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 . + /// + /// The option to change. + /// A new instance with the option changed. + public virtual RelationalOptionsExtension WithDataSource(DbDataSource? dataSource) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.DataSource = dataSource; + + return clone; + } + + /// + public override RelationalOptionsExtension WithConnectionString(string? connectionString) + { + var clone = (GaussDBOptionsExtension)base.WithConnectionString(connectionString); + + clone.DataSource = null; + + return clone; + } + + /// + public override RelationalOptionsExtension WithConnection(DbConnection? connection) + { + var clone = (GaussDBOptionsExtension)base.WithConnection(connection); + + clone.DataSource = null; + + return clone; + } + + /// + /// Returns a copy of the current instance configured with the specified range mapping. + /// + public virtual GaussDBOptionsExtension WithUserRangeDefinition( + string rangeName, + string? schemaName = null, + string? subtypeName = null) + => WithUserRangeDefinition(rangeName, schemaName, typeof(TSubtype), subtypeName); + + /// + /// Returns a copy of the current instance configured with the specified range mapping. + /// + public virtual GaussDBOptionsExtension WithUserRangeDefinition( + string rangeName, + string? schemaName, + Type subtypeClrType, + string? subtypeName) + { + Check.NotEmpty(rangeName, nameof(rangeName)); + Check.NotNull(subtypeClrType, nameof(subtypeClrType)); + + var clone = (GaussDBOptionsExtension)Clone(); + + clone._userRangeDefinitions.Add(new UserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName)); + + return clone; + } + + /// + /// Returns a copy of the current instance configured with the specified range mapping. + /// + public virtual GaussDBOptionsExtension WithEnumMapping( + Type clrType, + string? enumName, + string? schemaName, + IGaussDBNameTranslator? 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 = (GaussDBOptionsExtension)Clone(); + +#pragma warning disable CS0618 // GaussDBConnection.GlobalTypeMapper is obsolete + nameTranslator ??= GaussDBConnection.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. + /// + /// The name of the database for administrative operations. + public virtual GaussDBOptionsExtension WithAdminDatabase(string? adminDatabase) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.AdminDatabase = adminDatabase; + + return clone; + } + + /// + /// Returns a copy of the current instance with the specified GaussDB version. + /// + /// The backend version to target. + /// + /// A copy of the current instance with the specified GaussDB version. + /// + public virtual GaussDBOptionsExtension WithPostgresVersion(Version? postgresVersion) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone._postgresVersion = postgresVersion; + + return clone; + } + + /// + /// Returns a copy of the current instance with the specified Redshift settings. + /// + /// Whether to target Redshift. + /// + /// A copy of the current instance with the specified Redshift setting. + /// + public virtual GaussDBOptionsExtension WithRedshift(bool useRedshift) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.UseRedshift = useRedshift; + + return clone; + } + + /// + /// Returns a copy of the current instance configured with the specified value.. + /// + /// True to enable reverse null ordering; otherwise, false. + internal virtual GaussDBOptionsExtension WithReverseNullOrdering(bool reverseNullOrdering) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.ReverseNullOrdering = reverseNullOrdering; + + return clone; + } + + /// + public override void Validate(IDbContextOptions options) + { + base.Validate(options); + + // If we don't have an explicitly-configured data source, try to get one from the application service provider. + var dataSource = DataSource + ?? options.FindExtension()?.ApplicationServiceProvider?.GetService(); + + if (dataSource is not null) + { + if (ProvideClientCertificatesCallback is not null) + { + throw new InvalidOperationException( + "When passing an GaussDBDataSource to UseGaussDB(), call 'ProvideClientCertificatesCallback' on GaussDBDataSourceBuilder rather than in UseGaussDB()."); + } + + if (RemoteCertificateValidationCallback is not null) + { + throw new InvalidOperationException( + "When passing an GaussDBDataSource to UseGaussDB(), call 'RemoteCertificateValidationCallback' on GaussDBDataSourceBuilder rather than in UseGaussDB()."); + } + + if (ProvidePasswordCallback is not null) + { + throw new InvalidOperationException( + "When passing an GaussDBDataSource to UseGaussDB(), 'ProviderPasswordCallback' cannot be used in UseGaussDB(). See https://www.GaussDB.org/doc/security.html for configuring passwords and token rotation on GaussDBDataSourceBuilder."); + } + } + + if (UseRedshift && _postgresVersion is not null) + { + throw new InvalidOperationException($"{nameof(UseRedshift)} and {nameof(PostgresVersion)} cannot both be set"); + } + } + + #region Authentication + + /// + /// Returns a copy of the current instance with the specified . + /// + /// The specified callback. + public virtual GaussDBOptionsExtension WithProvideClientCertificatesCallback(ProvideClientCertificatesCallback? callback) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.ProvideClientCertificatesCallback = callback; + + return clone; + } + + /// + /// Returns a copy of the current instance with the specified . + /// + /// The specified callback. + public virtual GaussDBOptionsExtension WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback? callback) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.RemoteCertificateValidationCallback = callback; + + return clone; + } + + /// + /// Returns a copy of the current instance with the specified . + /// + /// The specified callback. +#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete + public virtual GaussDBOptionsExtension WithProvidePasswordCallback(ProvidePasswordCallback? callback) + { + var clone = (GaussDBOptionsExtension)Clone(); + + clone.ProvidePasswordCallback = callback; + + return clone; + } +#pragma warning restore CS0618 + + #endregion Authentication + + #region Infrastructure + + /// + protected override RelationalOptionsExtension Clone() + => new GaussDBOptionsExtension(this); + + /// + public override void ApplyServices(IServiceCollection services) + => services.AddEntityFrameworkGaussDB(); + + /// + public override DbContextOptionsExtensionInfo Info + => _info ??= new ExtensionInfo(this); + + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : RelationalExtensionInfo(extension) + { + private int? _serviceProviderHash; + private string? _logFragment; + + private new GaussDBOptionsExtension Extension + => (GaussDBOptionsExtension)base.Extension; + + public override bool IsDatabaseProvider + => true; + + public override string LogFragment + { + get + { + if (_logFragment is not null) + { + return _logFragment; + } + + var builder = new StringBuilder(base.LogFragment); + + if (Extension.AdminDatabase is not null) + { + builder.Append(nameof(Extension.AdminDatabase)).Append("=").Append(Extension.AdminDatabase).Append(' '); + } + + if (Extension._postgresVersion is not null) + { + builder.Append(nameof(Extension.PostgresVersion)).Append("=").Append(Extension.PostgresVersion).Append(' '); + } + + if (Extension.UseRedshift) + { + builder.Append(nameof(Extension.UseRedshift)).Append(' '); + } + + if (Extension.ProvideClientCertificatesCallback is not null) + { + builder.Append(nameof(Extension.ProvideClientCertificatesCallback)).Append(" "); + } + + if (Extension.RemoteCertificateValidationCallback is not null) + { + builder.Append(nameof(Extension.RemoteCertificateValidationCallback)).Append(" "); + } + + if (Extension.ProvidePasswordCallback is not null) + { + builder.Append(nameof(Extension.ProvidePasswordCallback)).Append(" "); + } + + if (Extension.ReverseNullOrdering) + { + builder.Append(nameof(Extension.ReverseNullOrdering)).Append(" "); + } + + if (Extension.UserRangeDefinitions.Count > 0) + { + builder.Append(nameof(Extension.UserRangeDefinitions)).Append("=["); + foreach (var item in Extension.UserRangeDefinitions) + { + builder.Append(item.SubtypeClrType).Append("=>"); + + if (item.StoreTypeSchema is not null) + { + builder.Append(item.StoreTypeSchema).Append("."); + } + + builder.Append(item.StoreTypeName); + + if (item.SubtypeName is not null) + { + builder.Append("(").Append(item.SubtypeName).Append(")"); + } + + builder.Append(";"); + } + + builder.Length -= 1; + builder.Append("] "); + } + + return _logFragment = builder.ToString(); + } + } + + public override int GetServiceProviderHashCode() + { + if (_serviceProviderHash is null) + { + var hashCode = new HashCode(); + + foreach (var userRangeDefinition in Extension._userRangeDefinitions) + { + hashCode.Add(userRangeDefinition); + } + + hashCode.Add(Extension.AdminDatabase); + hashCode.Add(Extension.PostgresVersion); + hashCode.Add(Extension.UseRedshift); + hashCode.Add(Extension.ProvideClientCertificatesCallback); + hashCode.Add(Extension.RemoteCertificateValidationCallback); + hashCode.Add(Extension.ProvidePasswordCallback); + hashCode.Add(Extension.ReverseNullOrdering); + + _serviceProviderHash = hashCode.ToHashCode(); + } + + return _serviceProviderHash.Value; + } + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + => other is ExtensionInfo otherInfo + && Extension.PostgresVersion == otherInfo.Extension.PostgresVersion + && Extension.ReverseNullOrdering == otherInfo.Extension.ReverseNullOrdering + && Extension.EnumDefinitions.SequenceEqual(otherInfo.Extension.EnumDefinitions) + && Extension.UserRangeDefinitions.SequenceEqual(otherInfo.Extension.UserRangeDefinitions) + && Extension.UseRedshift == otherInfo.Extension.UseRedshift; + + /// + public override void PopulateDebugInfo(IDictionary debugInfo) + { + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.UseAdminDatabase)] + = (Extension.AdminDatabase?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); + + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.SetPostgresVersion)] + = Extension.PostgresVersion.GetHashCode().ToString(CultureInfo.InvariantCulture); + + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.UseRedshift)] + = Extension.UseRedshift.GetHashCode().ToString(CultureInfo.InvariantCulture); + + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.ReverseNullOrdering)] + = Extension.ReverseNullOrdering.GetHashCode().ToString(CultureInfo.InvariantCulture); + + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.RemoteCertificateValidationCallback)] + = (Extension.RemoteCertificateValidationCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); + + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.ProvideClientCertificatesCallback)] + = (Extension.ProvideClientCertificatesCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); + + debugInfo["HuaweiCloud.EntityFrameworkCore.GaussDB:" + nameof(GaussDBDbContextOptionsBuilder.ProvidePasswordCallback)] + = (Extension.ProvidePasswordCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); + + foreach (var enumDefinition in Extension._enumDefinitions) + { + debugInfo[ + "HuaweiCloud.EntityFrameworkCore.GaussDB:" + + nameof(GaussDBDbContextOptionsBuilder.MapEnum) + + ":" + + enumDefinition.ClrType.Name] + = enumDefinition.GetHashCode().ToString(CultureInfo.InvariantCulture); + } + + foreach (var rangeDefinition in Extension._userRangeDefinitions) + { + debugInfo[ + "HuaweiCloud.EntityFrameworkCore.GaussDB:" + + nameof(GaussDBDbContextOptionsBuilder.MapRange) + + ":" + + rangeDefinition.SubtypeClrType.Name] + = rangeDefinition.GetHashCode().ToString(CultureInfo.InvariantCulture); + } + } + } + + #endregion Infrastructure +} diff --git a/src/EFCore.GaussDB/Infrastructure/Internal/IGaussDBSingletonOptions.cs b/src/EFCore.GaussDB/Infrastructure/Internal/IGaussDBSingletonOptions.cs new file mode 100644 index 0000000000..6b347f7e35 --- /dev/null +++ b/src/EFCore.GaussDB/Infrastructure/Internal/IGaussDBSingletonOptions.cs @@ -0,0 +1,37 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +/// +/// Represents options for GaussDB that can only be set at the singleton level. +/// +public interface IGaussDBSingletonOptions : ISingletonOptions +{ + /// + /// The backend version to target. + /// + Version PostgresVersion { get; } + + /// + /// Whether the user has explicitly set the backend version to target. + /// + bool IsPostgresVersionSet { get; } + + /// + /// Whether to target Redshift. + /// + bool UseRedshift { get; } + + /// + /// Whether reverse null ordering is enabled. + /// + bool ReverseNullOrderingEnabled { get; } + + /// + /// The collection of enum mappings. + /// + IReadOnlyList EnumDefinitions { get; } + + /// + /// The collection of range mappings. + /// + IReadOnlyList UserRangeDefinitions { get; } +} diff --git a/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs b/src/EFCore.GaussDB/Infrastructure/Internal/UserRangeDefinition.cs similarity index 88% rename from src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs rename to src/EFCore.GaussDB/Infrastructure/Internal/UserRangeDefinition.cs index 8c5908a286..b97cc0522b 100644 --- a/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs +++ b/src/EFCore.GaussDB/Infrastructure/Internal/UserRangeDefinition.cs @@ -1,7 +1,7 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; /// -/// A definition for a user-defined PostgreSQL range to be mapped. +/// A definition for a user-defined GaussDB range to be mapped. /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -12,7 +12,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; public sealed record UserRangeDefinition { /// - /// The name of the PostgreSQL range type to be mapped. + /// The name of the GaussDB range type to be mapped. /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -23,7 +23,7 @@ public sealed record UserRangeDefinition public string StoreTypeName { get; } /// - /// The PostgreSQL schema in which the range is defined. If null, the default schema is used + /// The GaussDB schema in which the range is defined. If null, the default schema is used /// (which is public unless changed on the model). /// /// @@ -36,7 +36,7 @@ public sealed record UserRangeDefinition /// /// The CLR type of the range's subtype (or element). - /// The actual mapped type will be an over this type. + /// 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 @@ -47,7 +47,7 @@ public sealed record UserRangeDefinition public Type SubtypeClrType { get; } /// - /// Optionally, the name of the range's PostgreSQL subtype (or element). + /// Optionally, the name of the range's GaussDB subtype (or element). /// This is usually not needed - the subtype will be inferred based on . /// /// diff --git a/src/EFCore.PG/Internal/EnumerableMethods.cs b/src/EFCore.GaussDB/Internal/EnumerableMethods.cs similarity index 99% rename from src/EFCore.PG/Internal/EnumerableMethods.cs rename to src/EFCore.GaussDB/Internal/EnumerableMethods.cs index 3d86265345..8c5bf38bc1 100644 --- a/src/EFCore.PG/Internal/EnumerableMethods.cs +++ b/src/EFCore.GaussDB/Internal/EnumerableMethods.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; internal static class EnumerableMethods { diff --git a/src/EFCore.GaussDB/Internal/GaussDBLoggerExtensions.cs b/src/EFCore.GaussDB/Internal/GaussDBLoggerExtensions.cs new file mode 100644 index 0000000000..4aa5278c43 --- /dev/null +++ b/src/EFCore.GaussDB/Internal/GaussDBLoggerExtensions.cs @@ -0,0 +1,239 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBLoggerExtensions +{ + /// + /// 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 void MissingSchemaWarning( + this IDiagnosticsLogger diagnostics, + string? schemaName) + { + var definition = GaussDBResources.LogMissingSchema(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, schemaName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void MissingTableWarning( + this IDiagnosticsLogger diagnostics, + string? tableName) + { + var definition = GaussDBResources.LogMissingTable(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, tableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void ForeignKeyReferencesMissingPrincipalTableWarning( + this IDiagnosticsLogger diagnostics, + string? foreignKeyName, + string? tableName, + string? principalTableName) + { + var definition = GaussDBResources.LogPrincipalTableNotInSelectionSet(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, foreignKeyName, tableName, principalTableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void ColumnFound( + this IDiagnosticsLogger diagnostics, + string tableName, + string columnName, + string dataTypeName, + bool nullable, + bool identity, + string? defaultValue, + string? computedValue) + { + var definition = GaussDBResources.LogFoundColumn(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + l => l.LogDebug( + definition.EventId, + null, + definition.MessageFormat, + tableName, + columnName, + dataTypeName, + nullable, + identity, + defaultValue, + computedValue)); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void CollationFound( + this IDiagnosticsLogger diagnostics, + string schema, + string collationName, + string lcCollate, + string lcCtype, + string? provider, + bool deterministic) + { + var definition = GaussDBResources.LogFoundCollation(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, collationName, schema, lcCollate, lcCtype, provider, deterministic); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void UniqueConstraintFound( + this IDiagnosticsLogger diagnostics, + string? uniqueConstraintName, + string tableName) + { + var definition = GaussDBResources.LogFoundUniqueConstraint(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, uniqueConstraintName, tableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void EnumColumnSkippedWarning( + this IDiagnosticsLogger diagnostics, + string columnName) + { + var definition = GaussDBResources.LogEnumColumnSkipped(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, columnName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void ExpressionIndexSkippedWarning( + this IDiagnosticsLogger diagnostics, + string indexName, + string tableName) + { + var definition = GaussDBResources.LogExpressionIndexSkipped(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, indexName, tableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void UnsupportedColumnIndexSkippedWarning( + this IDiagnosticsLogger diagnostics, + string indexName, + string tableName) + { + var definition = GaussDBResources.LogUnsupportedColumnIndexSkipped(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, indexName, tableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } + + /// + /// 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 void UnsupportedColumnConstraintSkippedWarning( + this IDiagnosticsLogger diagnostics, + string? indexName, + string tableName) + { + var definition = GaussDBResources.LogUnsupportedColumnConstraintSkipped(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, indexName, tableName); + } + + // No DiagnosticsSource events because these are purely design-time messages + } +} diff --git a/src/EFCore.GaussDB/Internal/GaussDBSingletonOptions.cs b/src/EFCore.GaussDB/Internal/GaussDBSingletonOptions.cs new file mode 100644 index 0000000000..1facdd57e1 --- /dev/null +++ b/src/EFCore.GaussDB/Internal/GaussDBSingletonOptions.cs @@ -0,0 +1,127 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBSingletonOptions : IGaussDBSingletonOptions +{ + /// + public virtual Version PostgresVersion { get; private set; } = 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 virtual bool IsPostgresVersionSet { 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 bool UseRedshift { 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 bool ReverseNullOrderingEnabled { 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 IReadOnlyList EnumDefinitions { 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 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 GaussDBSingletonOptions() + { + EnumDefinitions = []; + UserRangeDefinitions = []; + } + + /// + public virtual void Initialize(IDbContextOptions options) + { + var npgsqlOptions = options.FindExtension() ?? new GaussDBOptionsExtension(); + + PostgresVersion = npgsqlOptions.PostgresVersion; + IsPostgresVersionSet = npgsqlOptions.IsPostgresVersionSet; + UseRedshift = npgsqlOptions.UseRedshift; + ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering; + EnumDefinitions = npgsqlOptions.EnumDefinitions; + UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions; + } + + /// + public virtual void Validate(IDbContextOptions options) + { + var npgsqlOptions = options.FindExtension() ?? new GaussDBOptionsExtension(); + + if (PostgresVersion != npgsqlOptions.PostgresVersion) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(GaussDBDbContextOptionsBuilder.SetPostgresVersion), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + + if (UseRedshift != npgsqlOptions.UseRedshift) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(GaussDBDbContextOptionsBuilder.UseRedshift), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + + if (ReverseNullOrderingEnabled != npgsqlOptions.ReverseNullOrdering) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(GaussDBDbContextOptionsBuilder.ReverseNullOrdering), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + + if (!EnumDefinitions.SequenceEqual(npgsqlOptions.EnumDefinitions)) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(GaussDBDbContextOptionsBuilder.MapEnum), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + + if (!UserRangeDefinitions.SequenceEqual(npgsqlOptions.UserRangeDefinitions)) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(GaussDBDbContextOptionsBuilder.MapRange), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + } +} diff --git a/src/EFCore.PG/Internal/IReadOnlyListExtensions.cs b/src/EFCore.GaussDB/Internal/IReadOnlyListExtensions.cs similarity index 93% rename from src/EFCore.PG/Internal/IReadOnlyListExtensions.cs rename to src/EFCore.GaussDB/Internal/IReadOnlyListExtensions.cs index 4c4a1ae1a3..0696c7134e 100644 --- a/src/EFCore.PG/Internal/IReadOnlyListExtensions.cs +++ b/src/EFCore.GaussDB/Internal/IReadOnlyListExtensions.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; internal static class IReadOnlyListExtensions { diff --git a/src/EFCore.PG/Metadata/CockroachDbInterleaveInParent.cs b/src/EFCore.GaussDB/Metadata/CockroachDbInterleaveInParent.cs similarity index 98% rename from src/EFCore.PG/Metadata/CockroachDbInterleaveInParent.cs rename to src/EFCore.GaussDB/Metadata/CockroachDbInterleaveInParent.cs index fac2f97fd0..4ee68fbcde 100644 --- a/src/EFCore.PG/Metadata/CockroachDbInterleaveInParent.cs +++ b/src/EFCore.GaussDB/Metadata/CockroachDbInterleaveInParent.cs @@ -1,7 +1,7 @@ using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBConventionSetBuilder.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBConventionSetBuilder.cs new file mode 100644 index 0000000000..6e2e554521 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBConventionSetBuilder.cs @@ -0,0 +1,133 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A builder for building conventions for GaussDB. +/// +/// +/// +/// The service lifetime is and multiple registrations are allowed. This means that each +/// instance will use its own set of instances of this service. The implementations may depend on other +/// services registered with any lifetime. The implementations do not need to be thread-safe. +/// +/// +/// See Model building conventions, and +/// +/// +public class GaussDBConventionSetBuilder : RelationalConventionSetBuilder +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly Version _postgresVersion; + private readonly IReadOnlyList _enumDefinitions; + + /// + /// Creates a new instance. + /// + /// The core dependencies for this service. + /// The relational dependencies for this service. + /// The type mapping source to use. + /// The singleton options to use. + public GaussDBConventionSetBuilder( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies, + IRelationalTypeMappingSource typeMappingSource, + IGaussDBSingletonOptions npgsqlSingletonOptions) + : base(dependencies, relationalDependencies) + { + _typeMappingSource = typeMappingSource; + _postgresVersion = npgsqlSingletonOptions.PostgresVersion; + _enumDefinitions = npgsqlSingletonOptions.EnumDefinitions; + } + + /// + public override ConventionSet CreateConventionSet() + { + var conventionSet = base.CreateConventionSet(); + + var valueGenerationStrategyConvention = + new GaussDBValueGenerationStrategyConvention(Dependencies, RelationalDependencies, _postgresVersion); + conventionSet.ModelInitializedConventions.Add(valueGenerationStrategyConvention); + conventionSet.ModelInitializedConventions.Add( + new RelationalMaxIdentifierLengthConvention(63, Dependencies, RelationalDependencies)); + + ValueGenerationConvention valueGenerationConvention = new GaussDBValueGenerationConvention(Dependencies, RelationalDependencies); + ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, valueGenerationConvention); + + ReplaceConvention( + conventionSet.EntityTypeAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); + + ReplaceConvention(conventionSet.EntityTypePrimaryKeyChangedConventions, valueGenerationConvention); + + ReplaceConvention(conventionSet.ForeignKeyAddedConventions, valueGenerationConvention); + + ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention); + + var storeGenerationConvention = + new GaussDBStoreGenerationConvention(Dependencies, RelationalDependencies); + ReplaceConvention(conventionSet.PropertyAnnotationChangedConventions, storeGenerationConvention); + ReplaceConvention( + conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); + + conventionSet.ModelFinalizingConventions.Add(valueGenerationStrategyConvention); + conventionSet.ModelFinalizingConventions.Add(new GaussDBModelFinalizingConvention(_typeMappingSource, _enumDefinitions)); + ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention); + ReplaceConvention( + conventionSet.ModelFinalizingConventions, + (SharedTableConvention)new GaussDBSharedTableConvention(Dependencies, RelationalDependencies)); + + ReplaceConvention( + conventionSet.ModelFinalizedConventions, + (RuntimeModelConvention)new GaussDBRuntimeModelConvention(Dependencies, RelationalDependencies)); + + return conventionSet; + } + + /// + /// + /// Call this method to build a for GaussDB when using + /// the outside of . + /// + /// + /// Note that it is unusual to use this method. + /// Consider using in the normal way instead. + /// + /// + /// The convention set. + public static ConventionSet Build() + { + using var serviceScope = CreateServiceScope(); + using var context = serviceScope.ServiceProvider.GetRequiredService(); + return ConventionSet.CreateConventionSet(context); + } + + /// + /// + /// Call this method to build a for GaussDB outside of . + /// + /// + /// Note that it is unusual to use this method. + /// Consider using in the normal way instead. + /// + /// + /// The convention set. + public static ModelBuilder CreateModelBuilder() + { + using var serviceScope = CreateServiceScope(); + using var context = serviceScope.ServiceProvider.GetRequiredService(); + return new ModelBuilder(ConventionSet.CreateConventionSet(context), context.GetService()); + } + + private static IServiceScope CreateServiceScope() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkGaussDB() + .AddDbContext( + (p, o) => + o.UseGaussDB("Server=.") + .UseInternalServiceProvider(p)) + .BuildServiceProvider(); + + return serviceProvider.GetRequiredService().CreateScope(); + } +} diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBModelFinalizingConvention.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBModelFinalizingConvention.cs new file mode 100644 index 0000000000..99c805ed00 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBModelFinalizingConvention.cs @@ -0,0 +1,101 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A convention that discovers certain common GaussDB extensions based on store types used in the model (e.g. hstore). +/// +/// +/// See Model building conventions. +/// +public class GaussDBModelFinalizingConvention : IModelFinalizingConvention +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly IReadOnlyList _enumDefinitions; + + /// + /// Creates a new instance of . + /// + /// The type mapping source to use. + /// + public GaussDBModelFinalizingConvention( + IRelationalTypeMappingSource typeMappingSource, + IReadOnlyList enumDefinitions) + { + _typeMappingSource = typeMappingSource; + _enumDefinitions = enumDefinitions; + } + + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var property in entityType.GetDeclaredProperties()) + { + var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping() + ?? _typeMappingSource.FindMapping((IProperty)property); + + if (typeMapping is not null) + { + DiscoverPostgresExtensions(property, typeMapping, modelBuilder); + ProcessRowVersionProperty(property, typeMapping); + } + } + } + + SetupEnums(modelBuilder); + } + + /// + /// Configures the model to create GaussDB 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()); + } + } + + /// + /// Discovers certain common GaussDB extensions based on property store types (e.g. hstore). + /// + protected virtual void DiscoverPostgresExtensions( + IConventionProperty property, + RelationalTypeMapping typeMapping, + IConventionModelBuilder modelBuilder) + { + // TODO: does not work if CREATE EXTENSION was done on a non-default schema. #3177 + switch (typeMapping.StoreType) + { + case "hstore": + modelBuilder.HasPostgresExtension("hstore"); + break; + case "citext": + modelBuilder.HasPostgresExtension("citext"); + break; + case "ltree": + case "lquery": + case "ltxtquery": + modelBuilder.HasPostgresExtension("ltree"); + break; + } + } + + /// + /// Detects properties which are uint, OnAddOrUpdate and configured as concurrency tokens, and maps these to the GaussDB + /// internal "xmin" column, which changes every time the row is modified. + /// + protected virtual void ProcessRowVersionProperty(IConventionProperty property, RelationalTypeMapping typeMapping) + { + if (property is { ValueGenerated: ValueGenerated.OnAddOrUpdate, IsConcurrencyToken: true } + && typeMapping.StoreType == "xid") + { + property.Builder.HasColumnName("xmin"); + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBRuntimeModelConvention.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBRuntimeModelConvention.cs new file mode 100644 index 0000000000..6e146607c3 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBRuntimeModelConvention.cs @@ -0,0 +1,121 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A convention that creates an optimized copy of the mutable model. +/// +public class GaussDBRuntimeModelConvention : RelationalRuntimeModelConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public GaussDBRuntimeModelConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + protected override void ProcessModelAnnotations( + Dictionary annotations, + IModel model, + RuntimeModel runtimeModel, + bool runtime) + { + base.ProcessModelAnnotations(annotations, model, runtimeModel, runtime); + + if (!runtime) + { + annotations.Remove(GaussDBAnnotationNames.DatabaseTemplate); + annotations.Remove(GaussDBAnnotationNames.Tablespace); + annotations.Remove(GaussDBAnnotationNames.CollationDefinitionPrefix); + +#pragma warning disable CS0618 + annotations.Remove(GaussDBAnnotationNames.DefaultColumnCollation); +#pragma warning restore CS0618 + + foreach (var annotationName in annotations.Keys.Where( + k => + k.StartsWith(GaussDBAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) + || k.StartsWith(GaussDBAnnotationNames.EnumPrefix, StringComparison.Ordinal) + || k.StartsWith(GaussDBAnnotationNames.RangePrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + } + + /// + protected override void ProcessEntityTypeAnnotations( + Dictionary annotations, + IEntityType entityType, + RuntimeEntityType runtimeEntityType, + bool runtime) + { + base.ProcessEntityTypeAnnotations(annotations, entityType, runtimeEntityType, runtime); + + if (!runtime) + { + annotations.Remove(GaussDBAnnotationNames.UnloggedTable); + + foreach (var annotationName in annotations.Keys.Where( + k => k.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + } + + /// + protected override void ProcessPropertyAnnotations( + Dictionary annotations, + IProperty property, + RuntimeProperty runtimeProperty, + bool runtime) + { + base.ProcessPropertyAnnotations(annotations, property, runtimeProperty, runtime); + + if (!runtime) + { + annotations.Remove(GaussDBAnnotationNames.IdentityOptions); + annotations.Remove(GaussDBAnnotationNames.TsVectorConfig); + annotations.Remove(GaussDBAnnotationNames.TsVectorProperties); + + if (!annotations.ContainsKey(GaussDBAnnotationNames.ValueGenerationStrategy)) + { + annotations[GaussDBAnnotationNames.ValueGenerationStrategy] = property.GetValueGenerationStrategy(); + } + } + } + + /// + protected override void ProcessIndexAnnotations( + Dictionary annotations, + IIndex index, + RuntimeIndex runtimeIndex, + bool runtime) + { + base.ProcessIndexAnnotations(annotations, index, runtimeIndex, runtime); + + if (!runtime) + { + annotations.Remove(GaussDBAnnotationNames.IndexMethod); + annotations.Remove(GaussDBAnnotationNames.IndexOperators); + annotations.Remove(GaussDBAnnotationNames.IndexSortOrder); + annotations.Remove(GaussDBAnnotationNames.IndexNullSortOrder); + annotations.Remove(GaussDBAnnotationNames.IndexInclude); + annotations.Remove(GaussDBAnnotationNames.CreatedConcurrently); + annotations.Remove(GaussDBAnnotationNames.NullsDistinct); + + foreach (var annotationName in annotations.Keys.Where( + k => k.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + annotations.Remove(annotationName); + } + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBSharedTableConvention.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBSharedTableConvention.cs new file mode 100644 index 0000000000..7267a844e6 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBSharedTableConvention.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A convention that manipulates names of database objects for entity types that share a table to avoid clashes. +/// +public class GaussDBSharedTableConvention : SharedTableConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public GaussDBSharedTableConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + protected override bool AreCompatible(IReadOnlyIndex index, IReadOnlyIndex duplicateIndex, in StoreObjectIdentifier storeObject) + => base.AreCompatible(index, duplicateIndex, storeObject) + && index.AreCompatibleForGaussDB(duplicateIndex, storeObject, shouldThrow: false); + + /// + protected override bool CheckConstraintsUniqueAcrossTables + => false; +} diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBStoreGenerationConvention.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBStoreGenerationConvention.cs new file mode 100644 index 0000000000..6fefc9cebf --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBStoreGenerationConvention.cs @@ -0,0 +1,131 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A convention that ensures that properties aren't configured to have a default value, as computed column +/// or using a at the same time. +/// +public class GaussDBStoreGenerationConvention : StoreGenerationConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public GaussDBStoreGenerationConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + /// Called after an annotation is changed on a property. + /// + /// The builder for the property. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + public override void ProcessPropertyAnnotationChanged( + IConventionPropertyBuilder propertyBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context) + { + if (annotation is null + || oldAnnotation?.Value is not null) + { + return; + } + + var configurationSource = annotation.GetConfigurationSource(); + var fromDataAnnotation = configurationSource != ConfigurationSource.Convention; + switch (name) + { + case RelationalAnnotationNames.DefaultValue: + if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) is null + && propertyBuilder.HasDefaultValue(null, fromDataAnnotation) is not null) + { + context.StopProcessing(); + return; + } + + break; + case RelationalAnnotationNames.DefaultValueSql: + if (propertyBuilder.Metadata.GetValueGenerationStrategy() != GaussDBValueGenerationStrategy.Sequence + && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) == null + && propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) != null) + { + context.StopProcessing(); + return; + } + + break; + case RelationalAnnotationNames.ComputedColumnSql: + if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) is null + && propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) is not null) + { + context.StopProcessing(); + return; + } + + break; + case GaussDBAnnotationNames.ValueGenerationStrategy: + if (((propertyBuilder.Metadata.GetValueGenerationStrategy() != GaussDBValueGenerationStrategy.Sequence + && (propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null + || propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) == null + || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)) + || (propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null + || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)) + && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) != null) + { + context.StopProcessing(); + return; + } + + break; + } + + base.ProcessPropertyAnnotationChanged(propertyBuilder, name, annotation, oldAnnotation, context); + } + + /// + protected override void Validate(IConventionProperty property, in StoreObjectIdentifier storeObject) + { + if (property.GetValueGenerationStrategyConfigurationSource() is not null) + { + var generationStrategy = property.GetValueGenerationStrategy(storeObject); + if (generationStrategy == GaussDBValueGenerationStrategy.None) + { + base.Validate(property, storeObject); + return; + } + + if (property.TryGetDefaultValue(storeObject, out _)) + { + throw new InvalidOperationException( + RelationalStrings.ConflictingColumnServerGeneration( + "GaussDBValueGenerationStrategy", property.Name, "DefaultValue")); + } + + if (property.GetDefaultValueSql() is not null) + { + throw new InvalidOperationException( + RelationalStrings.ConflictingColumnServerGeneration( + "GaussDBValueGenerationStrategy", property.Name, "DefaultValueSql")); + } + + if (property.GetComputedColumnSql() is not null) + { + throw new InvalidOperationException( + RelationalStrings.ConflictingColumnServerGeneration( + "GaussDBValueGenerationStrategy", property.Name, "ComputedColumnSql")); + } + } + + base.Validate(property, storeObject); + } +} diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBValueGenerationConvention.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBValueGenerationConvention.cs new file mode 100644 index 0000000000..d0c17ac829 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBValueGenerationConvention.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A convention that configures store value generation as on properties that are +/// part of the primary key and not part of any foreign keys, were configured to have a database default value +/// or were configured to use a . +/// It also configures properties as if they were configured as computed columns. +/// +public class GaussDBValueGenerationConvention : RelationalValueGenerationConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public GaussDBValueGenerationConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + /// Called after an annotation is changed on a property. + /// + /// The builder for the property. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + public override void ProcessPropertyAnnotationChanged( + IConventionPropertyBuilder propertyBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context) + { + if (name == GaussDBAnnotationNames.ValueGenerationStrategy) + { + propertyBuilder.ValueGenerated(GetValueGenerated(propertyBuilder.Metadata)); + return; + } + + if (name == GaussDBAnnotationNames.TsVectorConfig && propertyBuilder.Metadata.GetTsVectorConfig() is not null) + { + propertyBuilder.ValueGenerated(ValueGenerated.OnAddOrUpdate); + return; + } + + base.ProcessPropertyAnnotationChanged(propertyBuilder, name, annotation, oldAnnotation, context); + } + + /// + /// Returns the store value generation strategy to set for the given property. + /// + /// The property. + /// The store value generation strategy to set for the given property. + protected override ValueGenerated? GetValueGenerated(IConventionProperty property) + { + // TODO: move to relational? + if (property.DeclaringType.IsMappedToJson() +#pragma warning disable EF1001 // Internal EF Core API usage. + && property.IsOrdinalKeyProperty() +#pragma warning restore EF1001 // Internal EF Core API usage. + && (property.DeclaringType as IReadOnlyEntityType)?.FindOwnership()!.IsUnique == false) + { + return ValueGenerated.OnAdd; + } + + var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); + if (declaringTable.Name == null) + { + return null; + } + + // If the first mapping can be value generated then we'll consider all mappings to be value generated + // as this is a client-side configuration and can't be specified per-table. + return GetValueGenerated(property, declaringTable, Dependencies.TypeMappingSource); + } + + /// + /// Returns the store value generation strategy to set for the given property. + /// + /// The property. + /// The identifier of the store object. + /// The store value generation strategy to set for the given property. + public static new ValueGenerated? GetValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) + ?? (property.GetValueGenerationStrategy(storeObject) != GaussDBValueGenerationStrategy.None + ? ValueGenerated.OnAdd + : null); + + private ValueGenerated? GetValueGenerated( + IReadOnlyProperty property, + in StoreObjectIdentifier storeObject, + ITypeMappingSource typeMappingSource) + => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) + ?? (property.GetValueGenerationStrategy(storeObject, typeMappingSource) != GaussDBValueGenerationStrategy.None + ? ValueGenerated.OnAdd + : null); +} diff --git a/src/EFCore.GaussDB/Metadata/Conventions/GaussDBValueGenerationStrategyConvention.cs b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBValueGenerationStrategyConvention.cs new file mode 100644 index 0000000000..f084f7449b --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Conventions/GaussDBValueGenerationStrategyConvention.cs @@ -0,0 +1,120 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +/// +/// A convention that configures the default model as +/// for newer GaussDB versions, +/// and for pre-10.0 versions. +/// +public class GaussDBValueGenerationStrategyConvention : IModelInitializedConvention, IModelFinalizingConvention +{ + private readonly Version? _postgresVersion; + + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + /// The GaussDB version being targeted. This affects the default value generation strategy. + public GaussDBValueGenerationStrategyConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies, + Version? postgresVersion) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + _postgresVersion = postgresVersion; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + public virtual void ProcessModelInitialized(IConventionModelBuilder modelBuilder, IConventionContext context) + => modelBuilder.HasValueGenerationStrategy( + _postgresVersion < new Version(10, 0) + ? GaussDBValueGenerationStrategy.SerialColumn + : GaussDBValueGenerationStrategy.IdentityByDefaultColumn); + + /// + public virtual void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var property in entityType.GetDeclaredProperties()) + { + GaussDBValueGenerationStrategy? strategy = null; + var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); + if (declaringTable.Name != null!) + { + strategy = property.GetValueGenerationStrategy(declaringTable, Dependencies.TypeMappingSource); + if (strategy == GaussDBValueGenerationStrategy.None + && !IsStrategyNoneNeeded(property, declaringTable)) + { + strategy = null; + } + } + else + { + var declaringView = property.GetMappedStoreObjects(StoreObjectType.View).FirstOrDefault(); + if (declaringView.Name != null!) + { + strategy = property.GetValueGenerationStrategy(declaringView, Dependencies.TypeMappingSource); + if (strategy == GaussDBValueGenerationStrategy.None + && !IsStrategyNoneNeeded(property, declaringView)) + { + strategy = null; + } + } + } + + // Needed for the annotation to show up in the model snapshot + if (strategy != null + && declaringTable.Name != null) + { + property.Builder.HasValueGenerationStrategy(strategy); + + if (strategy == GaussDBValueGenerationStrategy.Sequence) + { + var sequence = modelBuilder.HasSequence( + property.GetSequenceName(declaringTable) + ?? entityType.GetRootType().ShortName() + modelBuilder.Metadata.GetSequenceNameSuffix(), + property.GetSequenceSchema(declaringTable) + ?? modelBuilder.Metadata.GetSequenceSchema()).Metadata; + + property.Builder.HasDefaultValueSql( + RelationalDependencies.UpdateSqlGenerator.GenerateObtainNextSequenceValueOperation( + sequence.Name, sequence.Schema)); + } + } + } + } + + bool IsStrategyNoneNeeded(IReadOnlyProperty property, StoreObjectIdentifier storeObject) + { + if (property.ValueGenerated == ValueGenerated.OnAdd + && !property.TryGetDefaultValue(storeObject, out _) + && property.GetDefaultValueSql(storeObject) is null + && property.GetComputedColumnSql(storeObject) is null + && property.DeclaringType.Model.GetValueGenerationStrategy() != GaussDBValueGenerationStrategy.None) + { + var providerClrType = (property.GetValueConverter() + ?? (property.FindRelationalTypeMapping(storeObject) + ?? Dependencies.TypeMappingSource.FindMapping((IProperty)property))?.Converter) + ?.ProviderClrType.UnwrapNullableType(); + + return providerClrType is not null && (providerClrType.IsInteger()); + } + + return false; + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/GaussDBCollation.cs b/src/EFCore.GaussDB/Metadata/GaussDBCollation.cs new file mode 100644 index 0000000000..6850c71715 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/GaussDBCollation.cs @@ -0,0 +1,215 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +/// +/// 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 GaussDBCollation +{ + private readonly IReadOnlyAnnotatable _annotatable; + private readonly string _annotationName; + + internal GaussDBCollation(IReadOnlyAnnotatable annotatable, string annotationName) + { + _annotatable = Check.NotNull(annotatable, nameof(annotatable)); + _annotationName = Check.NotNull(annotationName, nameof(annotationName)); + } + + /// + /// 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 GaussDBCollation GetOrAddCollation( + IMutableAnnotatable annotatable, + string? schema, + string name, + string lcCollate, + string lcCtype, + string? provider = null, + bool? deterministic = null) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + if (FindCollation(annotatable, schema, name) is { } collation) + { + return collation; + } + + var annotationName = BuildAnnotationName(schema, name); + + return new GaussDBCollation(annotatable, annotationName) + { + LcCollate = lcCollate, + LcCtype = lcCtype, + Provider = provider, + IsDeterministic = deterministic + }; + } + + /// + /// 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 GaussDBCollation? FindCollation( + IReadOnlyAnnotatable annotatable, + string? schema, + string name) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + var annotationName = BuildAnnotationName(schema, name); + + return annotatable[annotationName] is null ? null : new GaussDBCollation(annotatable, annotationName); + } + + private static string BuildAnnotationName(string? schema, string name) + => schema is not null + ? $"{GaussDBAnnotationNames.CollationDefinitionPrefix}{schema}.{name}" + : $"{GaussDBAnnotationNames.CollationDefinitionPrefix}{name}"; + + /// + /// 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 IEnumerable GetCollations(IReadOnlyAnnotatable annotatable) + => Check.NotNull(annotatable, nameof(annotatable)) + .GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)) + .Select(a => new GaussDBCollation(annotatable, a.Name)); + + /// + /// 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 Annotatable Annotatable + => (Annotatable)_annotatable; + + /// + /// 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 string? Schema + => GetData().Schema; + + /// + /// 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 string Name + => GetData().Name!; + + /// + /// 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 string LcCollate + { + get => GetData().LcCollate!; + set => SetData(lcCollate: value); + } + + /// + /// 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 string LcCtype + { + get => GetData().LcCtype!; + set => SetData(lcCtype: value); + } + + /// + /// 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 string? Provider + { + get => GetData().Provider; + set => SetData(provider: value); + } + + /// + /// 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 bool? IsDeterministic + { + get => GetData().IsDeterministic; + set => SetData(deterministic: value); + } + + private (string? Schema, string? Name, string? LcCollate, string? LcCtype, string? Provider, bool? IsDeterministic) GetData() + => Deserialize(Annotatable.FindAnnotation(_annotationName)); + + private void SetData(string? lcCollate = null, string? lcCtype = null, string? provider = null, bool? deterministic = null) + => Annotatable[_annotationName] = + $"{lcCollate ?? LcCollate},{lcCtype ?? LcCtype},{provider ?? Provider},{deterministic ?? IsDeterministic}"; + + private static (string? Schema, string? Name, string? LcCollate, string? LcCtype, string? Provider, bool? IsDeterministic) + Deserialize(IAnnotation? annotation) + { + if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) + { + return (null, null!, null!, null!, null, null); + } + + string?[] elements = value.Split(','); + if (elements.Length != 4) + { + throw new ArgumentException($"Cannot parse collation annotation value: {value}"); + } + + for (var i = 0; i < 4; i++) + { + if (elements[i] == "") + { + elements[i] = null; + } + } + + var isDeterministic = elements[3] is { } isDeterminsticString + ? bool.Parse(isDeterminsticString) + : (bool?)null; + + // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs). + // Yes, this doesn't support dots in the schema/collation name, let somebody complain first. + var schemaAndName = annotation.Name.Substring(GaussDBAnnotationNames.CollationDefinitionPrefix.Length).Split('.'); + switch (schemaAndName.Length) + { + case 1: + return (null, schemaAndName[0], elements[0], elements[1], elements[2], isDeterministic); + case 2: + return (schemaAndName[0], schemaAndName[1], elements[0], elements[1], elements[2], isDeterministic); + default: + throw new ArgumentException($"Cannot parse collation name from annotation: {annotation.Name}"); + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/GaussDBEnum.cs b/src/EFCore.GaussDB/Metadata/GaussDBEnum.cs new file mode 100644 index 0000000000..08a78fd9b6 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/GaussDBEnum.cs @@ -0,0 +1,248 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +/// +/// Represents the metadata for a GaussDB enum. +/// +public class GaussDBEnum +{ + private readonly IReadOnlyAnnotatable _annotatable; + private readonly string _annotationName; + + /// + /// Creates a . + /// + /// The annotatable to search for the annotation. + /// The annotation name to search for in the annotatable. + /// + /// + /// + /// + /// + /// + internal GaussDBEnum(IReadOnlyAnnotatable annotatable, string annotationName) + { + _annotatable = Check.NotNull(annotatable, nameof(annotatable)); + _annotationName = Check.NotNull(annotationName, nameof(annotationName)); + } + + /// + /// 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 GaussDBEnum GetOrAddPostgresEnum( + IMutableAnnotatable 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 GaussDBEnum(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 GaussDBEnum 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 GaussDBEnum(annotatable, annotationName) { Labels = labels }; + } + + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the enum. + /// The enum name. + /// The enum labels. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBEnum GetOrAddPostgresEnum( + IMutableAnnotatable annotatable, + string name, + string[] labels) + => GetOrAddPostgresEnum(annotatable, null, name, labels); + + /// + /// Finds a in the , or returns null if not found. + /// + /// The annotatable to search for the enum. + /// The enum schema or null to use the model's default schema. + /// The enum name. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBEnum? FindPostgresEnum( + IReadOnlyAnnotatable annotatable, + string? schema, + string name) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + var annotationName = BuildAnnotationName(schema, name); + + return annotatable[annotationName] is null ? null : new GaussDBEnum(annotatable, annotationName); + } + + private static string BuildAnnotationName(string? schema, string name) + => schema is not null + ? $"{GaussDBAnnotationNames.EnumPrefix}{schema}.{name}" + : $"{GaussDBAnnotationNames.EnumPrefix}{name}"; + + /// + /// Gets the collection of stored in the . + /// + /// The annotatable to search for annotations. + /// + /// The collection of stored in the . + /// + /// + /// + /// + public static IEnumerable GetPostgresEnums(IReadOnlyAnnotatable annotatable) + => Check.NotNull(annotatable, nameof(annotatable)) + .GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.EnumPrefix, StringComparison.Ordinal)) + .Select(a => new GaussDBEnum(annotatable, a.Name)); + + /// + /// The that stores the enum. + /// + public virtual Annotatable Annotatable + => (Annotatable)_annotatable; + + /// + /// The enum schema or null to represent the default schema. + /// + public virtual string? Schema + => GetData().Schema; + + /// + /// The enum name. + /// + public virtual string Name + => GetData().Name!; + + /// + /// The enum labels. + /// + public virtual IReadOnlyList Labels + { + get => GetData().Labels!; + set => SetData(value); + } + + private (string? Schema, string? Name, string[]? Labels) GetData() + => Deserialize(Annotatable.FindAnnotation(_annotationName)); + + private void SetData(IEnumerable labels) + => Annotatable[_annotationName] = string.Join(",", labels); + + private static (string? Schema, string? Name, string[]? Labels) Deserialize(IAnnotation? annotation) + { + if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) + { + return (null, null, null); + } + + var labels = value.Split(','); + + // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs). + // Yes, this doesn't support dots in the schema/enum name, let somebody complain first. + var schemaAndName = annotation.Name.Substring(GaussDBAnnotationNames.EnumPrefix.Length).Split('.'); + switch (schemaAndName.Length) + { + case 1: + return (null, schemaAndName[0], labels); + case 2: + return (schemaAndName[0], schemaAndName[1], labels); + default: + throw new ArgumentException($"Cannot parse enum name from annotation: {annotation.Name}"); + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/GaussDBExtension.cs b/src/EFCore.GaussDB/Metadata/GaussDBExtension.cs new file mode 100644 index 0000000000..78e30459b5 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/GaussDBExtension.cs @@ -0,0 +1,238 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +/// +/// Represents the metadata for a GaussDB extension. +/// +public class GaussDBExtension +{ + private readonly IReadOnlyAnnotatable _annotatable; + private readonly string _annotationName; + + /// + /// Creates a . + /// + /// The annotatable to search for the annotation. + /// The annotation name to search for in the annotatable. + /// + /// + /// + /// + /// + /// + internal GaussDBExtension(IReadOnlyAnnotatable annotatable, string annotationName) + { + _annotatable = Check.NotNull(annotatable, nameof(annotatable)); + _annotationName = Check.NotNull(annotationName, nameof(annotationName)); + } + + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the extension. + /// The extension schema or null to use the model's default schema. + /// The extension name. + /// The extension version. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBExtension GetOrAddPostgresExtension( + IMutableAnnotatable annotatable, + string? schema, + string name, + string? version) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotNull(name, nameof(name)); + + if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) + { + return postgresExtension; + } + + var annotationName = BuildAnnotationName(schema, name); + + return new GaussDBExtension(annotatable, annotationName) { Version = version }; + } + + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the extension. + /// The extension schema or null to use the model's default schema. + /// The extension name. + /// The extension version. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBExtension GetOrAddPostgresExtension( + IConventionAnnotatable annotatable, + string? schema, + string name, + string? version) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotNull(name, nameof(name)); + + if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) + { + return postgresExtension; + } + + var annotationName = BuildAnnotationName(schema, name); + + return new GaussDBExtension(annotatable, annotationName) { Version = version }; + } + + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the extension. + /// The extension name. + /// The extension version. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + public static GaussDBExtension GetOrAddPostgresExtension( + IMutableAnnotatable annotatable, + string name, + string? version) + => GetOrAddPostgresExtension(annotatable, null, name, version); + + /// + /// Finds a in the , or returns null if not found. + /// + /// The annotatable to search for the extension. + /// The extension schema. The default schema is never used. + /// The extension name. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBExtension? FindPostgresExtension( + IReadOnlyAnnotatable annotatable, + string? schema, + string name) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + var annotationName = BuildAnnotationName(schema, name); + + return annotatable[annotationName] is null ? null : new GaussDBExtension(annotatable, annotationName); + } + + internal static string BuildAnnotationName(string? schema, string name) + => schema is not null + ? $"{GaussDBAnnotationNames.PostgresExtensionPrefix}{schema}.{name}" + : $"{GaussDBAnnotationNames.PostgresExtensionPrefix}{name}"; + + /// + /// Gets the collection of stored in the . + /// + /// The annotatable to search for annotations. + /// + /// The collection of stored in the . + /// + /// + /// + /// + public static IEnumerable GetPostgresExtensions(IReadOnlyAnnotatable annotatable) + => Check.NotNull(annotatable, nameof(annotatable)) + .GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)) + .Select(a => new GaussDBExtension(annotatable, a.Name)); + + /// + /// The that stores the extension. + /// + public virtual Annotatable Annotatable + => (Annotatable)_annotatable; + + /// + /// The extension schema or null to represent the default schema. + /// + public virtual string? Schema + => GetData().Schema; + + /// + /// The extension name. + /// + public virtual string Name + => GetData().Name!; + + /// + /// The extension version. + /// + public virtual string? Version + { + get => GetData().Version; + set => SetData(value); + } + + private (string? Schema, string? Name, string? Version) GetData() + => Deserialize(Annotatable.FindAnnotation(_annotationName)!); + + private void SetData(string? version) + { + var data = GetData(); + Annotatable[_annotationName] = $"{data.Schema},{data.Name},{version}"; + } + + private static (string? Schema, string? Name, string? Version) Deserialize(IAnnotation? annotation) + { + if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) + { + return (null, null, null); + } + + // TODO: Can't actually use schema and name...they might not be set when this is first called. + var schemaNameValue = value.Split(',').Select(x => x.Trim()).Select(x => x is "" or "''" ? null : x).ToArray(); + var schemaAndName = annotation.Name.Substring(GaussDBAnnotationNames.PostgresExtensionPrefix.Length).Split('.'); + switch (schemaAndName.Length) + { + case 1: + return (null, schemaAndName[0], schemaNameValue[2]); + case 2: + return (schemaAndName[0], schemaAndName[1], schemaNameValue[2]); + default: + throw new ArgumentException($"Cannot parse extension name from annotation: {annotation.Name}"); + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/GaussDBRange.cs b/src/EFCore.GaussDB/Metadata/GaussDBRange.cs new file mode 100644 index 0000000000..286b153a91 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/GaussDBRange.cs @@ -0,0 +1,253 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +/// +/// Represents the metadata for a GaussDB range. +/// +public class GaussDBRange +{ + private readonly IReadOnlyAnnotatable _annotatable; + private readonly string _annotationName; + + /// + /// Creates a . + /// + /// The annotatable to search for the annotation. + /// The annotation name to search for in the annotatable. + /// + /// + /// + /// + /// + /// + internal GaussDBRange(IReadOnlyAnnotatable annotatable, string annotationName) + { + _annotatable = Check.NotNull(annotatable, nameof(annotatable)); + _annotationName = Check.NotNull(annotationName, nameof(annotationName)); + } + + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the range. + /// The range schema or null to use the model's default schema. + /// The range name. + /// The range subtype. + /// + /// + /// + /// + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBRange GetOrAddPostgresRange( + IMutableAnnotatable annotatable, + string? schema, + string name, + string subtype, + string? canonicalFunction = null, + string? subtypeOpClass = null, + string? collation = null, + string? subtypeDiff = null) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + Check.NotNull(subtype, nameof(subtype)); + + if (FindPostgresRange(annotatable, schema, name) is { } postgresRange) + { + return postgresRange; + } + + var annotationName = BuildAnnotationName(schema, name); + + return new GaussDBRange(annotatable, annotationName) + { + Subtype = subtype, + CanonicalFunction = canonicalFunction, + SubtypeOpClass = subtypeOpClass, + Collation = collation, + SubtypeDiff = subtypeDiff, + }; + } + + /// + /// Finds a in the , or returns null if not found. + /// + /// The annotatable to search for the range. + /// The range schema or null to use the model's default schema. + /// The range name. + /// + /// The from the . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static GaussDBRange? FindPostgresRange( + IReadOnlyAnnotatable annotatable, + string? schema, + string name) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotEmpty(name, nameof(name)); + + var annotationName = BuildAnnotationName(schema, name); + + return annotatable[annotationName] is null ? null : new GaussDBRange(annotatable, annotationName); + } + + private static string BuildAnnotationName(string? schema, string name) + => schema is not null + ? $"{GaussDBAnnotationNames.RangePrefix}{schema}.{name}" + : $"{GaussDBAnnotationNames.RangePrefix}{name}"; + + /// + /// Gets the collection of stored in the . + /// + /// The annotatable to search for annotations. + /// + /// The collection of stored in the . + /// + /// + /// + /// + public static IEnumerable GetPostgresRanges(IReadOnlyAnnotatable annotatable) + => Check.NotNull(annotatable, nameof(annotatable)) + .GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.RangePrefix, StringComparison.Ordinal)) + .Select(a => new GaussDBRange(annotatable, a.Name)); + + /// + /// The that stores the range. + /// + public virtual Annotatable Annotatable + => (Annotatable)_annotatable; + + /// + /// The range schema or null to represent the default schema. + /// + public virtual string? Schema + => GetData().Schema; + + /// + /// The range name. + /// + public virtual string Name + => GetData().Name!; + + /// + /// The subtype of the range. + /// + public virtual string Subtype + { + get => GetData().Subtype!; + set => SetData(subtype: value); + } + + /// + /// The function defining a "step" in a discrete range. + /// + public virtual string? CanonicalFunction + { + get => GetData().CanonicalFunction; + set => SetData(canonicalFunction: value); + } + + /// + /// The operator class to use. + /// + public virtual string? SubtypeOpClass + { + get => GetData().SubtypeOpClass; + set => SetData(subtypeOpClass: value); + } + + /// + /// The collation to use. + /// + public virtual string? Collation + { + get => GetData().Collation; + set => SetData(collation: value); + } + + /// + /// The function defining a difference in subtype values. + /// + public virtual string? SubtypeDiff + { + get => GetData().SubtypeDiff; + set => SetData(subtypeDiff: value); + } + + private (string? Schema, string? Name, string? Subtype, string? CanonicalFunction, string? SubtypeOpClass, string? Collation, string? + SubtypeDiff) GetData() + => Deserialize(Annotatable.FindAnnotation(_annotationName)!); + + private void SetData( + string? subtype = null, + string? canonicalFunction = null, + string? subtypeOpClass = null, + string? collation = null, + string? subtypeDiff = null) + => Annotatable[_annotationName] = + $"{subtype ?? Subtype},{canonicalFunction ?? CanonicalFunction},{subtypeOpClass ?? SubtypeOpClass},{collation ?? Collation},{subtypeDiff ?? SubtypeDiff}"; + + private static (string? Schema, string? Name, string? Subtype, string? CanonicalFunction, string? SubtypeOpClass, string? Collation, + string? SubtypeDiff) + Deserialize(IAnnotation? annotation) + { + if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) + { + return (null, null, null, null, null, null, null); + } + + string?[] elements = value.Split(','); + if (elements.Length != 5) + { + throw new ArgumentException($"Cannot parse range annotation value: {value}"); + } + + for (var i = 0; i < 5; i++) + { + if (elements[i] == "") + { + elements[i] = null; + } + } + + // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs). + // Yes, this doesn't support dots in the schema/range name, let somebody complain first. + var schemaAndName = annotation.Name.Substring(GaussDBAnnotationNames.RangePrefix.Length).Split('.'); + switch (schemaAndName.Length) + { + case 1: + return (null, schemaAndName[0], elements[0], elements[1], elements[2], elements[3], elements[4]); + case 2: + return (schemaAndName[0], schemaAndName[1], elements[0], elements[1], elements[2], elements[3], elements[4]); + default: + throw new ArgumentException($"Cannot parse range name from annotation: {annotation.Name}"); + } + } +} diff --git a/src/EFCore.GaussDB/Metadata/GaussDBValueGenerationStrategy.cs b/src/EFCore.GaussDB/Metadata/GaussDBValueGenerationStrategy.cs new file mode 100644 index 0000000000..c886742f3b --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/GaussDBValueGenerationStrategy.cs @@ -0,0 +1,73 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +/// +/// Defines strategies to use when generating values for database columns. +/// +/// +/// See Model building conventions. +/// +public enum GaussDBValueGenerationStrategy +{ + /// + /// No GaussDB-specific strategy. + /// + None, + + /// + /// + /// A sequence-based hi-lo pattern where blocks of IDs are allocated from the server and + /// used client-side for generating keys. + /// + /// + /// This is an advanced pattern--only use this strategy if you are certain it is what you need. + /// + /// + SequenceHiLo, + + /// + /// + /// Selects the serial column strategy, which is a regular column backed by an auto-created index. + /// + /// + /// If you are creating a new project on GaussDB 10 or above, consider using instead. + /// + /// + SerialColumn, + + /// + /// Selects the always-identity column strategy (a value cannot be provided). + /// Available only starting GaussDB 10. + /// + IdentityAlwaysColumn, + + /// + /// Selects the by-default-identity column strategy (a value can be provided to override the identity mechanism). + /// Available only starting GaussDB 10. + /// + IdentityByDefaultColumn, + + /// + /// A pattern that uses a database sequence to generate values for the column. + /// + Sequence +} + +/// +/// Extension methods over . +/// +public static class GaussDBValueGenerationStrategyExtensions +{ + /// + /// Whether the given strategy is either or + /// . + /// + public static bool IsIdentity(this GaussDBValueGenerationStrategy strategy) + => strategy is GaussDBValueGenerationStrategy.IdentityByDefaultColumn or GaussDBValueGenerationStrategy.IdentityAlwaysColumn; + + /// + /// Whether the given strategy is either or + /// . + /// + public static bool IsIdentity(this GaussDBValueGenerationStrategy? strategy) + => strategy is GaussDBValueGenerationStrategy.IdentityByDefaultColumn or GaussDBValueGenerationStrategy.IdentityAlwaysColumn; +} diff --git a/src/EFCore.PG/Metadata/Internal/CockroachDbAnnotationNames.cs b/src/EFCore.GaussDB/Metadata/Internal/CockroachDbAnnotationNames.cs similarity index 90% rename from src/EFCore.PG/Metadata/Internal/CockroachDbAnnotationNames.cs rename to src/EFCore.GaussDB/Metadata/Internal/CockroachDbAnnotationNames.cs index dcbeca820b..1b02766139 100644 --- a/src/EFCore.PG/Metadata/Internal/CockroachDbAnnotationNames.cs +++ b/src/EFCore.GaussDB/Metadata/Internal/CockroachDbAnnotationNames.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -14,7 +14,7 @@ public static class CockroachDbAnnotationNames /// 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 const string Prefix = NpgsqlAnnotationNames.Prefix + "CockroachDB:"; + public const string Prefix = GaussDBAnnotationNames.Prefix + "CockroachDB:"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.GaussDB/Metadata/Internal/GaussDBAnnotationNames.cs b/src/EFCore.GaussDB/Metadata/Internal/GaussDBAnnotationNames.cs new file mode 100644 index 0000000000..74c6fbd136 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Internal/GaussDBAnnotationNames.cs @@ -0,0 +1,274 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.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 GaussDBAnnotationNames +{ + /// + /// 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 const string Prefix = "GaussDB:"; + + /// + /// 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 const string CompressionMethod = Prefix + "Compression:"; + + /// + /// 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 const string CreatedConcurrently = Prefix + "CreatedConcurrently"; + + /// + /// 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 const string DatabaseTemplate = Prefix + "DatabaseTemplate"; + + /// + /// 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 const string HiLoSequenceName = Prefix + "HiLoSequenceName"; + + /// + /// 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 const string HiLoSequenceSchema = Prefix + "HiLoSequenceSchema"; + + /// + /// 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 const string IdentityOptions = Prefix + "IdentitySequenceOptions"; + + /// + /// 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 const string IndexMethod = Prefix + "IndexMethod"; + + /// + /// 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 const string IndexOperators = Prefix + "IndexOperators"; + + /// + /// 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 const string IndexNullSortOrder = Prefix + "IndexNullSortOrder"; + + /// + /// 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 const string IndexInclude = Prefix + "IndexInclude"; + + /// + /// 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 const string NullsDistinct = Prefix + "NullsDistinct"; + + /// + /// 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 const string Tablespace = Prefix + "Tablespace"; + + /// + /// 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 const string TsVectorConfig = Prefix + "TsVectorConfig"; + + /// + /// 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 const string TsVectorProperties = Prefix + "TsVectorProperties"; + + /// + /// 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 const string UnloggedTable = Prefix + "UnloggedTable"; + + /// + /// 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 const string ValueGenerationStrategy = Prefix + "ValueGenerationStrategy"; + + /// + /// 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 const string SequenceNameSuffix = Prefix + "SequenceNameSuffix"; + + /// + /// 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 const string SequenceName = Prefix + "SequenceName"; + + /// + /// 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 const string SequenceSchema = Prefix + "SequenceSchema"; + + /// + /// 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 const string CollationDefinitionPrefix = Prefix + "CollationDefinition:"; + + /// + /// 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 const string EnumPrefix = Prefix + "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 const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; + + /// + /// 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 const string RangePrefix = Prefix + "Range:"; + + /// + /// 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 const string StorageParameterPrefix = Prefix + "StorageParameter:"; + + // Database model annotations + + /// + /// Identifies the type of the GaussDB type of this column (e.g. array, range, base). + /// + public const string PostgresTypeType = Prefix + "PostgresTypeType"; + + /// + /// If this column's data type is an array, contains the data type of its elements. + /// Otherwise null. + /// + public const string ElementDataType = Prefix + "ElementDataType"; + + /// + /// If the index contains an expression (rather than simple column references), the expression is contained here. + /// This is currently unsupported and will be ignored. + /// + public const string IndexExpression = Prefix + "IndexExpression"; + + /// + /// 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 EF Core's standard model bulk configuration API")] + public const string DefaultColumnCollation = Prefix + "DefaultColumnCollation"; + + /// + /// 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("Replaced by ValueGenerationStrategy.SerialColumn")] + public const string ValueGeneratedOnAdd = Prefix + "ValueGeneratedOnAdd"; + + /// + /// 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("Replaced by built-in EF Core support, use HasComment on entities or properties.")] + public const string Comment = Prefix + "Comment"; + + /// + /// 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("Replaced by RelationalAnnotationNames.Collation")] + public const string IndexCollation = Prefix + "IndexCollation"; + + /// + /// 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. + /// + // Replaced by IsDescending in EF Core 7.0 + public const string IndexSortOrder = Prefix + "IndexSortOrder"; +} diff --git a/src/EFCore.GaussDB/Metadata/Internal/GaussDBAnnotationProvider.cs b/src/EFCore.GaussDB/Metadata/Internal/GaussDBAnnotationProvider.cs new file mode 100644 index 0000000000..ba26e3b0b5 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Internal/GaussDBAnnotationProvider.cs @@ -0,0 +1,223 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.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 GaussDBAnnotationProvider : RelationalAnnotationProvider +{ + /// + /// 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 GaussDBAnnotationProvider(RelationalAnnotationProviderDependencies dependencies) + : base(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 + /// 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 IEnumerable For(ITable table, bool designTime) + { + if (!designTime) + { + yield break; + } + + // Model validation ensures that these facets are the same on all mapped entity types + var entityType = (IEntityType)table.EntityTypeMappings.First().TypeBase; + + if (entityType.GetIsUnlogged()) + { + yield return new Annotation(GaussDBAnnotationNames.UnloggedTable, entityType.GetIsUnlogged()); + } + + if (entityType[CockroachDbAnnotationNames.InterleaveInParent] is not null) + { + yield return new Annotation( + CockroachDbAnnotationNames.InterleaveInParent, entityType[CockroachDbAnnotationNames.InterleaveInParent]); + } + + foreach (var storageParamAnnotation in entityType.GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + yield return storageParamAnnotation; + } + } + + /// + /// 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 IEnumerable For(IColumn column, bool designTime) + { + if (!designTime) + { + yield break; + } + + var table = StoreObjectIdentifier.Table(column.Table.Name, column.Table.Schema); + var valueGeneratedProperty = column.PropertyMappings.Where( + m => (m.TableMapping.IsSharedTablePrincipal ?? true) + && m.TableMapping.TypeBase == m.Property.DeclaringType) + .Select(m => m.Property) + .FirstOrDefault( + p => p.GetValueGenerationStrategy(table) switch + { + GaussDBValueGenerationStrategy.IdentityByDefaultColumn => true, + GaussDBValueGenerationStrategy.IdentityAlwaysColumn => true, + GaussDBValueGenerationStrategy.SerialColumn => true, + _ => false + }); + + if (valueGeneratedProperty is not null) + { + var valueGenerationStrategy = valueGeneratedProperty.GetValueGenerationStrategy(); + yield return new Annotation(GaussDBAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy); + + if (valueGenerationStrategy is GaussDBValueGenerationStrategy.IdentityByDefaultColumn + or GaussDBValueGenerationStrategy.IdentityAlwaysColumn) + { + if (valueGeneratedProperty[GaussDBAnnotationNames.IdentityOptions] is string identityOptions) + { + yield return new Annotation(GaussDBAnnotationNames.IdentityOptions, identityOptions); + } + } + } + + if (column.PropertyMappings.Select(m => m.Property.GetTsVectorConfig()) + .FirstOrDefault(c => c is not null) is { } tsVectorConfig) + { + yield return new Annotation(GaussDBAnnotationNames.TsVectorConfig, tsVectorConfig); + } + + valueGeneratedProperty = column.PropertyMappings.Select(m => m.Property) + .FirstOrDefault(p => p.GetTsVectorProperties() is not null); + if (valueGeneratedProperty is not null) + { + var tableIdentifier = StoreObjectIdentifier.Table(column.Table.Name, column.Table.Schema); + + yield return new Annotation( + GaussDBAnnotationNames.TsVectorProperties, + valueGeneratedProperty.GetTsVectorProperties()! + .Select(p2 => valueGeneratedProperty.DeclaringType.FindProperty(p2)!.GetColumnName(tableIdentifier)) + .ToArray()); + } + + // JSON columns have no property mappings so all annotations that rely on property mappings should be skipped for them + if (column is not JsonColumn + && column.PropertyMappings.FirstOrDefault()?.Property.GetCompressionMethod() is { } compressionMethod) + { + // Model validation ensures that these facets are the same on all mapped properties + yield return new Annotation(GaussDBAnnotationNames.CompressionMethod, compressionMethod); + } + } + + /// + /// 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 IEnumerable For(ITableIndex index, bool designTime) + { + if (!designTime) + { + yield break; + } + + // Model validation ensures that these facets are the same on all mapped indexes + var modelIndex = index.MappedIndexes.First(); + + if (modelIndex.GetCollation() is { } collation) + { + yield return new Annotation(RelationalAnnotationNames.Collation, collation); + } + + if (modelIndex.GetMethod() is { } method) + { + yield return new Annotation(GaussDBAnnotationNames.IndexMethod, method); + } + + if (modelIndex.GetOperators() is { } operators) + { + yield return new Annotation(GaussDBAnnotationNames.IndexOperators, operators); + } + + if (modelIndex.GetNullSortOrder() is { } nullSortOrder) + { + yield return new Annotation(GaussDBAnnotationNames.IndexNullSortOrder, nullSortOrder); + } + + if (modelIndex.GetTsVectorConfig() is { } configName) + { + yield return new Annotation(GaussDBAnnotationNames.TsVectorConfig, configName); + } + + if (modelIndex.GetIncludeProperties() is { } includeProperties) + { + var tableIdentifier = StoreObjectIdentifier.Table(index.Table.Name, index.Table.Schema); + + yield return new Annotation( + GaussDBAnnotationNames.IndexInclude, + includeProperties + .Select(p => modelIndex.DeclaringEntityType.FindProperty(p)!.GetColumnName(tableIdentifier)) + .ToArray()); + } + + if (modelIndex.IsCreatedConcurrently() is { } isCreatedConcurrently) + { + yield return new Annotation(GaussDBAnnotationNames.CreatedConcurrently, isCreatedConcurrently); + } + + if (modelIndex.GetAreNullsDistinct() is { } nullsDistinct) + { + yield return new Annotation(GaussDBAnnotationNames.NullsDistinct, nullsDistinct); + } + + foreach (var storageParamAnnotation in modelIndex.GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) + { + yield return storageParamAnnotation; + } + + // Support legacy annotation for index ordering + if (modelIndex[GaussDBAnnotationNames.IndexSortOrder] is IReadOnlyList legacySortOrder) + { + yield return new Annotation(GaussDBAnnotationNames.IndexSortOrder, legacySortOrder); + } + } + + /// + /// 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 IEnumerable For(IRelationalModel model, bool designTime) + { + if (!designTime) + { + return []; + } + + return model.Model.GetAnnotations().Where( + a => + a.Name.StartsWith(GaussDBAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) + || a.Name.StartsWith(GaussDBAnnotationNames.EnumPrefix, StringComparison.Ordinal) + || a.Name.StartsWith(GaussDBAnnotationNames.RangePrefix, StringComparison.Ordinal) + || a.Name.StartsWith(GaussDBAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)); + } +} diff --git a/src/EFCore.GaussDB/Metadata/Internal/GaussDBIndexExtensions.cs b/src/EFCore.GaussDB/Metadata/Internal/GaussDBIndexExtensions.cs new file mode 100644 index 0000000000..ac021654f9 --- /dev/null +++ b/src/EFCore.GaussDB/Metadata/Internal/GaussDBIndexExtensions.cs @@ -0,0 +1,104 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Metadata.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 GaussDBIndexExtensions +{ + /// + /// 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 AreCompatibleForGaussDB( + this IReadOnlyIndex index, + IReadOnlyIndex duplicateIndex, + in StoreObjectIdentifier storeObject, + bool shouldThrow) + { + if (index.GetIncludeProperties() != duplicateIndex.GetIncludeProperties()) + { + if (index.GetIncludeProperties() is null + || duplicateIndex.GetIncludeProperties() is null + || !SameColumnNames(index, duplicateIndex, storeObject)) + { + if (shouldThrow) + { + throw new InvalidOperationException( + GaussDBStrings.DuplicateIndexIncludedMismatch( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + duplicateIndex.Properties.Format(), + duplicateIndex.DeclaringEntityType.DisplayName(), + index.DeclaringEntityType.GetSchemaQualifiedTableName(), + index.GetDatabaseName(storeObject), + FormatInclude(index, storeObject), + FormatInclude(duplicateIndex, storeObject))); + } + + return false; + } + } + + if (index.IsCreatedConcurrently() != duplicateIndex.IsCreatedConcurrently()) + { + if (shouldThrow) + { + throw new InvalidOperationException( + GaussDBStrings.DuplicateIndexConcurrentCreationMismatch( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + duplicateIndex.Properties.Format(), + duplicateIndex.DeclaringEntityType.DisplayName(), + index.DeclaringEntityType.GetSchemaQualifiedTableName(), + index.GetDatabaseName(storeObject))); + } + + return false; + } + + if (index.GetCollation() != duplicateIndex.GetCollation()) + { + if (shouldThrow) + { + throw new InvalidOperationException( + GaussDBStrings.DuplicateIndexCollationMismatch( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + duplicateIndex.Properties.Format(), + duplicateIndex.DeclaringEntityType.DisplayName(), + index.DeclaringEntityType.GetSchemaQualifiedTableName(), + index.GetDatabaseName(storeObject))); + } + + return false; + } + + return true; + + static bool SameColumnNames(IReadOnlyIndex index, IReadOnlyIndex duplicateIndex, StoreObjectIdentifier storeObject) + => index.GetIncludeProperties()!.Select( + p => index.DeclaringEntityType.FindProperty(p)!.GetColumnName(storeObject)) + .SequenceEqual( + duplicateIndex.GetIncludeProperties()!.Select( + p => duplicateIndex.DeclaringEntityType.FindProperty(p)!.GetColumnName(storeObject))); + } + + private static string FormatInclude(IReadOnlyIndex index, StoreObjectIdentifier storeObject) + => index.GetIncludeProperties() is null + ? "{}" + : "{'" + + string.Join( + "', '", + index.GetIncludeProperties()!.Select( + p => index.DeclaringEntityType.FindProperty(p) + ?.GetColumnName(storeObject))) + + "'}"; +} diff --git a/src/EFCore.PG/Metadata/Internal/IdentitySequenceOptionsData.cs b/src/EFCore.GaussDB/Metadata/Internal/IdentitySequenceOptionsData.cs similarity index 98% rename from src/EFCore.PG/Metadata/Internal/IdentitySequenceOptionsData.cs rename to src/EFCore.GaussDB/Metadata/Internal/IdentitySequenceOptionsData.cs index 3f6eead87c..e96fe9d7a5 100644 --- a/src/EFCore.PG/Metadata/Internal/IdentitySequenceOptionsData.cs +++ b/src/EFCore.GaussDB/Metadata/Internal/IdentitySequenceOptionsData.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Text; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -99,7 +99,7 @@ public virtual string Serialize() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public static IdentitySequenceOptionsData Get(IReadOnlyAnnotatable annotatable) - => Deserialize((string?)annotatable[NpgsqlAnnotationNames.IdentityOptions]); + => Deserialize((string?)annotatable[GaussDBAnnotationNames.IdentityOptions]); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG/Metadata/NullSortOrder.cs b/src/EFCore.GaussDB/Metadata/NullSortOrder.cs similarity index 92% rename from src/EFCore.PG/Metadata/NullSortOrder.cs rename to src/EFCore.GaussDB/Metadata/NullSortOrder.cs index ac1ed5d11f..45391d7ce8 100644 --- a/src/EFCore.PG/Metadata/NullSortOrder.cs +++ b/src/EFCore.GaussDB/Metadata/NullSortOrder.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; /// /// Options for modifying sort ordering of NULL-values in indexes. diff --git a/src/EFCore.PG/Metadata/SortOrder.cs b/src/EFCore.GaussDB/Metadata/SortOrder.cs similarity index 85% rename from src/EFCore.PG/Metadata/SortOrder.cs rename to src/EFCore.GaussDB/Metadata/SortOrder.cs index a9c27c420d..f99ee7e44f 100644 --- a/src/EFCore.PG/Metadata/SortOrder.cs +++ b/src/EFCore.GaussDB/Metadata/SortOrder.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; /// /// Options for modifying sort ordering of index values. diff --git a/src/EFCore.GaussDB/Migrations/GaussDBMigrationsSqlGenerator.cs b/src/EFCore.GaussDB/Migrations/GaussDBMigrationsSqlGenerator.cs new file mode 100644 index 0000000000..c80be8fa18 --- /dev/null +++ b/src/EFCore.GaussDB/Migrations/GaussDBMigrationsSqlGenerator.cs @@ -0,0 +1,2320 @@ +using System.Globalization; +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Operations; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Update.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations; + +/// +/// GaussDB-specific implementation of . +/// +/// +/// +/// The service lifetime is . This means that each instance will use +/// its own instance of this service. The implementation may depend on other services registered with any lifetime. The +/// implementation does not need to be thread-safe. +/// +/// +/// See Database migrations. +/// +/// +public class GaussDBMigrationsSqlGenerator : MigrationsSqlGenerator +{ + private IReadOnlyList _operations = null!; + private readonly RelationalTypeMapping _stringTypeMapping; + + /// + /// The backend version to target. + /// + private readonly Version _postgresVersion; + + /// + /// Creates a new instance. + /// + /// Parameter object containing dependencies for this service. + /// The singleton options to use. + public GaussDBMigrationsSqlGenerator( + MigrationsSqlGeneratorDependencies dependencies, + IGaussDBSingletonOptions npgsqlSingletonOptions) + : base(dependencies) + { + _postgresVersion = npgsqlSingletonOptions.PostgresVersion; + _stringTypeMapping = dependencies.TypeMappingSource.GetMapping(typeof(string)) + ?? throw new InvalidOperationException("No string type mapping found"); + } + + /// + public override IReadOnlyList Generate( + IReadOnlyList operations, + IModel? model = null, + MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) + { + IReadOnlyList results; + _operations = operations; + + try + { + results = base.Generate(operations, model, options); + + AddSequenceBumpingForSeeding(); + + return results; + } + finally + { + _operations = null!; + } + + void AddSequenceBumpingForSeeding() + { + // For all tables where we had data seeding insertions, find all columns mapped to properties with identity/serial value + // generation strategy. We'll bump the sequences for those columns. + var seededGeneratedColumns = operations + .OfType() + .Select(o => new { o.Schema, o.Table }) + .Distinct() + .SelectMany( + t => model?.GetRelationalModel().FindTable(t.Table, t.Schema)?.Columns + .Where( + c => c.PropertyMappings.Any( + p => p.Property.GetValueGenerationStrategy() is + GaussDBValueGenerationStrategy.IdentityByDefaultColumn + or GaussDBValueGenerationStrategy.IdentityAlwaysColumn + or GaussDBValueGenerationStrategy.SerialColumn)) + ?? []) + .Distinct() + .ToArray(); + + if (!seededGeneratedColumns.Any()) + { + return; + } + + var builder = new MigrationCommandListBuilder(Dependencies); + + foreach (var c in seededGeneratedColumns) + { + // Weirdly, pg_get_serial_sequence accepts a standard quoted "schema"."table" inside its first + // parameter string literal, but the second one is a column name that shouldn't be double-quoted... + + var table = Dependencies.SqlGenerationHelper.DelimitIdentifier(c.Table.Name, c.Table.Schema); + var column = Dependencies.SqlGenerationHelper.DelimitIdentifier(c.Name); + var unquotedColumn = c.Name.Replace("'", "''"); + + // When generating idempotent scripts, migration DDL is enclosed in anonymous DO blocks, + // where PERFORM must be used instead of SELECT + var selectOrPerform = options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) + ? "PERFORM" + : "SELECT"; + + // Set the sequence's value to the greater of: + // 1. Maximum value currently present in the column (i.e. just seeded) + // 2. Current value of the sequence (the max value above could be out of range of the sequence, + // e.g. negative values seeded) + builder + .AppendLine( + $""" +{selectOrPerform} setval( + pg_get_serial_sequence('{table}', '{unquotedColumn}'), + GREATEST( + (SELECT MAX({column}) FROM {table}) + 1, + nextval(pg_get_serial_sequence('{table}', '{unquotedColumn}'))), + false); +"""); + } + + builder.EndCommand(); + + results = results.Concat(builder.GetCommandList()).ToArray(); + } + } + + /// + protected override void Generate(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation is GaussDBCreateDatabaseOperation createDatabaseOperation) + { + Generate(createDatabaseOperation, model, builder); + return; + } + + if (operation is GaussDBDropDatabaseOperation dropDatabaseOperation) + { + Generate(dropDatabaseOperation, model, builder); + return; + } + + base.Generate(operation, model, builder); + } + + #region Standard migrations + + /// + protected override void Generate( + CreateTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + if (!terminate && operation.Comment is not null) + { + throw new ArgumentException( + $"When generating migrations SQL for {nameof(CreateTableOperation)}, can't produce unterminated SQL with comments"); + } + + operation.Columns.RemoveAll(c => IsSystemColumn(c.Name)); + + builder.Append("CREATE "); + + if (operation[GaussDBAnnotationNames.UnloggedTable] is true) + { + builder.Append("UNLOGGED "); + } + + builder + .Append("TABLE ") + .Append(DelimitIdentifier(operation.Name, operation.Schema)) + .AppendLine(" ("); + + using (builder.Indent()) + { + CreateTableColumns(operation, model, builder); + CreateTableConstraints(operation, model, builder); + builder.AppendLine(); + } + + builder.Append(")"); + + // CockroachDB "interleave in parent" (https://www.cockroachlabs.com/docs/stable/interleave-in-parent.html) + if (operation[CockroachDbAnnotationNames.InterleaveInParent] is string) + { + var interleaveInParent = new CockroachDbInterleaveInParent(operation); + var parentTableSchema = interleaveInParent.ParentTableSchema; + var parentTableName = interleaveInParent.ParentTableName; + var interleavePrefix = interleaveInParent.InterleavePrefix; + + builder + .AppendLine() + .Append("INTERLEAVE IN PARENT ") + .Append(DelimitIdentifier(parentTableName, parentTableSchema)) + .Append(" (") + .Append(string.Join(", ", interleavePrefix.Select(c => DelimitIdentifier(c)))) + .Append(")"); + } + + AppendStoreParameters(operation, builder, withLeadingNewline: true); + + // Comment on the table + if (operation.Comment is not null) + { + builder.AppendLine(";"); + + builder + .Append("COMMENT ON TABLE ") + .Append(DelimitIdentifier(operation.Name, operation.Schema)) + .Append(" IS ") + .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)); + } + + // Comments on the columns + foreach (var columnOp in operation.Columns.Where(c => c.Comment is not null)) + { + var columnComment = columnOp.Comment; + builder.AppendLine(";"); + + builder + .Append("COMMENT ON COLUMN ") + .Append(DelimitIdentifier(operation.Name, operation.Schema)) + .Append(".") + .Append(DelimitIdentifier(columnOp.Name)) + .Append(" IS ") + .Append(_stringTypeMapping.GenerateSqlLiteral(columnComment)); + } + + if (terminate) + { + builder.AppendLine(";"); + EndStatement(builder); + } + } + + /// + protected override void Generate(AlterTableOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + var alterTableBaseSql = $"ALTER TABLE {DelimitIdentifier(operation.Name, operation.Schema)}"; + var madeChanges = false; + + // Storage parameters + madeChanges |= AppendStorageParameterAlterations(operation.OldTable, operation, alterTableBaseSql, builder); + + // Comment + if (operation.Comment != operation.OldTable.Comment) + { + builder + .Append("COMMENT ON TABLE ") + .Append(DelimitIdentifier(operation.Name, operation.Schema)) + .Append(" IS ") + .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)); + + builder.AppendLine(";"); + madeChanges = true; + } + + // Unlogged table (null is equivalent to false) + var oldUnlogged = operation.OldTable[GaussDBAnnotationNames.UnloggedTable] is true; + var newUnlogged = operation[GaussDBAnnotationNames.UnloggedTable] is true; + + if (oldUnlogged != newUnlogged) + { + builder + .Append(alterTableBaseSql) + .Append(" SET ") + .Append(newUnlogged ? "UNLOGGED" : "LOGGED") + .AppendLine(";"); + + madeChanges = true; + } + + if (madeChanges) + { + EndStatement(builder); + } + } + + /// + protected override void Generate( + DropColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + // Never touch system columns + if (IsSystemColumn(operation.Name)) + { + return; + } + + base.Generate(operation, model, builder, terminate); + } + + /// + protected override void Generate( + AddColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + if (!terminate && operation.Comment is not null) + { + throw new ArgumentException( + $"When generating migrations SQL for {nameof(AddColumnOperation)}, can't produce unterminated SQL with comments"); + } + + // Never touch system columns + if (IsSystemColumn(operation.Name)) + { + return; + } + + if (operation[GaussDBAnnotationNames.ValueGenerationStrategy] is GaussDBValueGenerationStrategy strategy) + { + switch (strategy) + { + case GaussDBValueGenerationStrategy.SerialColumn: + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + // NB: This gets added to all added non-nullable columns by MigrationsModelDiffer. We need to suppress + // it, here because PG can't have both IDENTITY/SERIAL and a DEFAULT constraint on the same column. + operation.DefaultValue = null; + break; + } + } + + base.Generate(operation, model, builder, terminate: false); + + if (operation.Comment is not null) + { + builder.AppendLine(";"); + + builder + .Append("COMMENT ON COLUMN ") + .Append(DelimitIdentifier(operation.Table, operation.Schema)) + .Append(".") + .Append(DelimitIdentifier(operation.Name)) + .Append(" IS ") + .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)); + } + + if (terminate) + { + builder.AppendLine(";"); + EndStatement(builder); + } + } + + /// + protected override void Generate(AlterColumnOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + // Never touch system columns + if (IsSystemColumn(operation.Name)) + { + return; + } + + var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) + ?.Columns.FirstOrDefault(c => c.Name == operation.Name); + + ApplyTsVectorColumnSql(operation, model, operation.Name, operation.Schema, operation.Table); + + // Note: OldColumn doesn't have Schema, Table or Name populated (https://github.com/dotnet/efcore/issues/28041), so we take these + // from the new column (they're identical in any case). + ApplyTsVectorColumnSql(operation.OldColumn, model, operation.Name, operation.Schema, operation.Table); + + if (operation.ComputedColumnSql != operation.OldColumn.ComputedColumnSql + || operation.IsStored != operation.OldColumn.IsStored) + { + // TODO: The following will fail if the column being altered is part of an index. + // SqlServer recreates indexes, but wait to see if GaussDB will introduce a proper ALTER TABLE ALTER COLUMN + // that allows us to do this cleanly. + var dropColumnOperation = new DropColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name + }; + + if (column is not null) + { + dropColumnOperation.AddAnnotations(column.GetAnnotations()); + } + + Generate(dropColumnOperation, model, builder); + + var addColumnOperation = new AddColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name, + ClrType = operation.ClrType, + ColumnType = operation.ColumnType, + IsUnicode = operation.IsUnicode, + IsFixedLength = operation.IsFixedLength, + MaxLength = operation.MaxLength, + Precision = operation.Precision, + Scale = operation.Scale, + IsRowVersion = operation.IsRowVersion, + IsNullable = operation.IsNullable, + DefaultValue = operation.DefaultValue, + DefaultValueSql = operation.DefaultValueSql, + ComputedColumnSql = operation.ComputedColumnSql, + IsStored = operation.IsStored, + Comment = operation.Comment, + Collation = operation.Collation + }; + addColumnOperation.AddAnnotations(operation.GetAnnotations()); + Generate(addColumnOperation, model, builder); + RecreateIndexes(column, operation, builder); + builder.EndCommand(); + + return; + } + + string? newSequenceName = null; + + var alterBase = $"ALTER TABLE {DelimitIdentifier(operation.Table, operation.Schema)} " + + $"ALTER COLUMN {DelimitIdentifier(operation.Name)} "; + + // TYPE + COLLATION + var type = operation.ColumnType ?? GetColumnType(operation.Schema, operation.Table, operation.Name, operation, model)!; + var oldType = IsOldColumnSupported(model) + ? operation.OldColumn.ColumnType ?? GetColumnType(operation.Schema, operation.Table, operation.Name, operation.OldColumn, model) + : null; + + // If a collation was defined on the column specifically, via the standard EF mechanism, it will be + // available in operation.Collation (as usual). + // If not, there may be a model-wide default column collation, which gets transmitted via the GaussDB-specific annotation. + // 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 + var oldCollation = (string?)(operation.OldColumn.Collation ?? operation.OldColumn[GaussDBAnnotationNames.DefaultColumnCollation]); + var newCollation = (string?)(operation.Collation ?? operation[GaussDBAnnotationNames.DefaultColumnCollation]); +#pragma warning restore CS0618 + + if (type != oldType || newCollation != oldCollation) + { + builder + .Append(alterBase) + .Append("TYPE ") + .Append(type); + + if (newCollation is not null) + { + 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(";"); + } + + if (operation is { IsNullable: true, OldColumn.IsNullable: false }) + { + builder + .Append(alterBase) + .Append("DROP NOT NULL") + .AppendLine(";"); + } + else if (operation is { IsNullable: false, OldColumn.IsNullable: true }) + { + // The column is being made non-nullable. Generate an update statement before doing that, to convert any existing null values to + // the default value (otherwise GaussDB fails). + if (operation.DefaultValueSql is not null || operation.DefaultValue is not null) + { + string defaultValueSql; + if (operation.DefaultValueSql is not null) + { + defaultValueSql = operation.DefaultValueSql; + } + else + { + Check.DebugAssert(operation.DefaultValue is not null, "operation.DefaultValue is not null"); + + var typeMapping = (type != null + ? Dependencies.TypeMappingSource.FindMapping(operation.DefaultValue.GetType(), type) + : null) + ?? Dependencies.TypeMappingSource.GetMappingForValue(operation.DefaultValue); + + defaultValueSql = typeMapping.GenerateSqlLiteral(operation.DefaultValue); + } + + builder + .Append("UPDATE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" SET ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" = ") + .Append(defaultValueSql) + .Append(" WHERE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" IS NULL") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + builder + .Append(alterBase) + .Append("SET NOT NULL") + .AppendLine(";"); + } + + // Compression + var oldCompressionMethod = (string?)operation.OldColumn[GaussDBAnnotationNames.CompressionMethod]; + var newCompressionMethod = (string?)operation[GaussDBAnnotationNames.CompressionMethod]; + + if (newCompressionMethod != oldCompressionMethod) + { + builder + .Append(alterBase) + .Append("SET COMPRESSION ") + .Append(newCompressionMethod ?? "default"); + } + + CheckForOldValueGenerationAnnotation(operation); + + var oldStrategy = operation.OldColumn[GaussDBAnnotationNames.ValueGenerationStrategy] as GaussDBValueGenerationStrategy?; + var newStrategy = operation[GaussDBAnnotationNames.ValueGenerationStrategy] as GaussDBValueGenerationStrategy?; + + if (oldStrategy != newStrategy) + { + // We have a value generation strategy change + + if (oldStrategy == GaussDBValueGenerationStrategy.SerialColumn) + { + // TODO: It would be better to actually select for the owned sequence. + // This would require plpgsql. + var sequence = DelimitIdentifier($"{operation.Table}_{operation.Name}_seq", operation.Schema); + switch (newStrategy) + { + case null: + // Drop the serial, converting the column to a regular int + builder.AppendLine($"DROP SEQUENCE {sequence} CASCADE;"); + break; + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + // Convert serial column to identity, maintaining the current sequence value + var identityTypeClause = newStrategy == GaussDBValueGenerationStrategy.IdentityAlwaysColumn + ? "ALWAYS" + : "BY DEFAULT"; + var oldSequence = DelimitIdentifier($"{operation.Table}_{operation.Name}_old_seq", operation.Schema); + var oldSequenceWithoutSchema = DelimitIdentifier($"{operation.Table}_{operation.Name}_old_seq"); + builder + .AppendLine($"ALTER SEQUENCE {sequence} RENAME TO {oldSequenceWithoutSchema};") + .AppendLine($"{alterBase}DROP DEFAULT;") + .AppendLine($"{alterBase}ADD GENERATED {identityTypeClause} AS IDENTITY;") + // When generating idempotent scripts, migration DDL is enclosed in anonymous DO blocks, + // where PERFORM must be used instead of SELECT + .Append(Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) ? "PERFORM" : "SELECT") + .AppendLine($" * FROM setval('{sequence}', nextval('{oldSequence}'), false);") + .AppendLine($"DROP SEQUENCE {oldSequence};"); + break; + default: + throw new NotSupportedException($"Don't know how to migrate serial column to {newStrategy}"); + } + } + else if (oldStrategy.IsIdentity()) + { + switch (newStrategy) + { + case null: + // Drop the identity, converting the column to a regular int + builder.Append(alterBase).AppendLine("DROP IDENTITY;"); + break; + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + builder.Append(alterBase).AppendLine("SET GENERATED ALWAYS;"); + break; + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + builder.Append(alterBase).AppendLine("SET GENERATED BY DEFAULT;"); + break; + case GaussDBValueGenerationStrategy.SerialColumn: + throw new NotSupportedException("Migrating from identity to serial isn't currently supported (and is a bad idea)"); + default: + throw new NotSupportedException($"Don't know how to migrate identity column to {newStrategy}"); + } + } + else if (oldStrategy is null) + { + switch (newStrategy) + { + case GaussDBValueGenerationStrategy.IdentityAlwaysColumn: + case GaussDBValueGenerationStrategy.IdentityByDefaultColumn: + builder.Append(alterBase).AppendLine("DROP DEFAULT;"); + builder.Append(alterBase).Append("ADD"); + IdentityDefinition(operation, builder); + builder.AppendLine(";"); + break; + case GaussDBValueGenerationStrategy.SerialColumn: + switch (type) + { + case "integer": + case "int": + case "int4": + case "bigint": + case "int8": + case "smallint": + case "int2": + newSequenceName = $"{operation.Table}_{operation.Name}_seq"; + Generate( + new CreateSequenceOperation + { + Schema = operation.Schema, + Name = newSequenceName, + ClrType = operation.ClrType + }, model, builder); + + builder.Append(alterBase).Append("SET"); + DefaultValue(null, $@"nextval('{DelimitIdentifier(newSequenceName, operation.Schema)}')", type, builder); + builder.AppendLine(";"); + // Note: we also need to set the sequence ownership, this is done below after the ALTER COLUMN + break; + } + + break; + default: + throw new NotSupportedException($"Don't know how to apply value generation strategy {newStrategy}"); + } + } + } + + // Identity sequence options may have changed + if (oldStrategy.IsIdentity() && newStrategy.IsIdentity()) + { + var newSequenceOptions = IdentitySequenceOptionsData.Get(operation); + var oldSequenceOptions = IdentitySequenceOptionsData.Get(operation.OldColumn); + + if (newSequenceOptions.StartValue != oldSequenceOptions.StartValue) + { + var startValue = newSequenceOptions.StartValue ?? 1; + + builder + .Append(alterBase) + .Append("RESTART WITH ") + .Append(startValue.ToString(CultureInfo.InvariantCulture)) + .AppendLine(";"); + } + + if (newSequenceOptions.IncrementBy != oldSequenceOptions.IncrementBy) + { + builder + .Append(alterBase) + .Append("SET INCREMENT BY ") + .Append(newSequenceOptions.IncrementBy.ToString(CultureInfo.InvariantCulture)) + .AppendLine(";"); + } + + if (newSequenceOptions.MinValue != oldSequenceOptions.MinValue) + { + builder + .Append(alterBase) + .Append( + newSequenceOptions.MinValue is null + ? "SET NO MINVALUE" + : "SET MINVALUE " + newSequenceOptions.MinValue) + .AppendLine(";"); + } + + if (newSequenceOptions.MaxValue != oldSequenceOptions.MaxValue) + { + builder + .Append(alterBase) + .Append( + newSequenceOptions.MaxValue is null + ? "SET NO MAXVALUE" + : "SET MAXVALUE " + newSequenceOptions.MaxValue) + .AppendLine(";"); + } + + if (newSequenceOptions.IsCyclic != oldSequenceOptions.IsCyclic) + { + builder + .Append(alterBase) + .Append( + newSequenceOptions.IsCyclic + ? "SET CYCLE" + : "SET NO CYCLE") + .AppendLine(";"); + } + + if (newSequenceOptions.NumbersToCache != oldSequenceOptions.NumbersToCache) + { + builder + .Append(alterBase) + .Append("SET CACHE ") + .Append(newSequenceOptions.NumbersToCache.ToString(CultureInfo.InvariantCulture)) + .AppendLine(";"); + } + } + + // DEFAULT. + // Note that defaults values for value-generated columns (identity, serial) are managed above. This is + // only for regular columns with user-specified default settings. + if (newStrategy is null + && (operation.DefaultValueSql != operation.OldColumn.DefaultValueSql + || !Equals(operation.DefaultValue, operation.OldColumn.DefaultValue))) + { + builder.Append(alterBase); + if (operation.DefaultValue is not null || operation.DefaultValueSql is not null) + { + builder.Append("SET"); + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, type, builder); + } + else + { + builder.Append("DROP DEFAULT"); + } + + builder.AppendLine(";"); + } + + // A sequence has been created because this column was altered to be a serial. + // Change the sequence's ownership. + if (newSequenceName is not null) + { + builder + .Append("ALTER SEQUENCE ") + .Append(DelimitIdentifier(newSequenceName, operation.Schema)) + .Append(" OWNED BY ") + .Append(DelimitIdentifier(operation.Table, operation.Schema)) + .Append(".") + .Append(DelimitIdentifier(operation.Name)) + .AppendLine(";"); + } + + // Comment + if (operation.Comment != operation.OldColumn.Comment) + { + builder + .Append("COMMENT ON COLUMN ") + .Append(DelimitIdentifier(operation.Table, operation.Schema)) + .Append(".") + .Append(DelimitIdentifier(operation.Name)) + .Append(" IS ") + .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)) + .AppendLine(";"); + } + + EndStatement(builder); + } + + /// + protected override void Generate(RenameIndexOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation.NewName is not null && operation.NewName != operation.Name) + { + Rename(operation.Schema, operation.Name, operation.NewName, "INDEX", builder); + } + + // N.B. indexes are always stored in the same schema as the table. + EndStatement(builder); + } + + /// + protected override void Generate(RenameSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + var name = operation.Name; + if (operation.NewName is not null && operation.NewName != operation.Name) + { + Rename(operation.Schema, operation.Name, operation.NewName, "SEQUENCE", builder); + + name = operation.NewName; + } + + if (operation.NewSchema is not null && operation.NewSchema != operation.Schema) + { + Transfer(operation.NewSchema, operation.Schema, name, "SEQUENCE", builder); + } + + 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) + { + // GaussDB has ALTER SEQUENCE ... RESTART WITH x, which resets the current sequence value but does not change its start value + // in the schema (so a subsequence RESTART without an argument resets it back to its original start value, not to x). + // It also has ALTER SEQUENCE ... STARTS WITH x, which resets the schema start value but not the current value. + // So we use both statements to reset both the current value and the schema value. + if (operation.StartValue.HasValue) + { + var longTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(long)); + + builder + .Append("ALTER SEQUENCE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) + .Append(" START WITH ") + .Append(longTypeMapping.GenerateSqlLiteral(operation.StartValue.Value)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + builder + .Append("ALTER SEQUENCE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) + .Append(" RESTART") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + else + { + builder + .Append("ALTER SEQUENCE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) + .Append(" RESTART") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + EndStatement(builder); + } + + /// + protected override void Generate(RenameTableOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + var name = operation.Name; + if (operation.NewName is not null && operation.NewName != operation.Name) + { + Rename(operation.Schema, operation.Name, operation.NewName, "TABLE", builder); + + name = operation.NewName; + } + + if (operation.NewSchema is not null && operation.NewSchema != operation.Schema) + { + Transfer(operation.NewSchema, operation.Schema, name, "TABLE", builder); + } + + EndStatement(builder); + } + + /// + protected override void Generate( + CreateIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder.Append("CREATE "); + + if (operation.IsUnique) + { + builder.Append("UNIQUE "); + } + + builder.Append("INDEX "); + + var concurrently = operation[GaussDBAnnotationNames.CreatedConcurrently] as bool? == true; + if (concurrently) + { + builder.Append("CONCURRENTLY "); + } + + builder + .Append(DelimitIdentifier(operation.Name)) + .Append(" ON ") + .Append(DelimitIdentifier(operation.Table, operation.Schema)); + + var method = operation[GaussDBAnnotationNames.IndexMethod] as string; + if (method?.Length > 0) + { + builder.Append(" USING ").Append(method); + } + + var indexColumns = GetIndexColumns(operation); + + var columnsExpression = operation[GaussDBAnnotationNames.TsVectorConfig] is string tsVectorConfig + ? ColumnsToTsVector(operation.Name, indexColumns.Select(i => i.Name), tsVectorConfig, model, operation.Schema, operation.Table) + : IndexColumnList(indexColumns, method); + + builder + .Append(" (") + .Append(columnsExpression) + .Append(")"); + + IndexOptions(operation, model, builder); + + if (terminate) + { + builder.AppendLine(";"); + // Concurrent indexes cannot be created within a transaction + EndStatement(builder, suppressTransaction: concurrently); + } + } + + /// + protected override void IndexOptions(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (_postgresVersion.AtLeast(11) && operation[GaussDBAnnotationNames.IndexInclude] is string[] { Length: > 0 } includeColumns) + { + builder + .Append(" INCLUDE (") + .Append(ColumnList(includeColumns)) + .Append(")"); + } + + if (operation[GaussDBAnnotationNames.NullsDistinct] is false) + { + builder.Append(" NULLS NOT DISTINCT"); + } + + AppendStoreParameters(operation, builder, withLeadingNewline: false); + + base.IndexOptions(operation, model, builder); + } + + /// + protected override void Generate(EnsureSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation.Name == "public") + { + return; + } + + // GaussDB has CREATE SCHEMA IF NOT EXISTS, but that requires CREATE privileges on the database even if the schema already + // exists. This blocks multi-tenant scenarios where the user has no database privileges. + // So we procedurally check if the schema exists instead, and create it if not. + var schemaName = operation.Name.Replace("'", "''"); + + // If we're generating an idempotent migration, we're already in a PL/PGSQL DO block; otherwise we need to start one. + if (!Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + builder + .AppendLine(@"DO $EF$") + .AppendLine("BEGIN"); + } + + builder + .AppendLine($" IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = '{schemaName}') THEN") + .AppendLine($" CREATE SCHEMA {DelimitIdentifier(operation.Name)};") + .AppendLine(" END IF;"); + + if (!Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + builder.AppendLine("END $EF$;"); + } + + EndStatement(builder); + } + + /// + protected virtual void Generate(GaussDBCreateDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder + .Append("CREATE DATABASE ") + .Append(DelimitIdentifier(operation.Name)); + + if (!string.IsNullOrEmpty(operation.Template)) + { + builder + .AppendLine() + .Append("TEMPLATE ") + .Append(DelimitIdentifier(operation.Template)); + } + + if (!string.IsNullOrEmpty(operation.Tablespace)) + { + builder + .AppendLine() + .Append("TABLESPACE ") + .Append(DelimitIdentifier(operation.Tablespace)); + } + + if (!string.IsNullOrEmpty(operation.Collation)) + { + builder + .AppendLine() + .Append("LC_COLLATE ") + .Append(DelimitIdentifier(operation.Collation)); + } + + builder.AppendLine(";"); + + EndStatement(builder, suppressTransaction: true); + } + + /// + public virtual void Generate(GaussDBDropDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + var dbName = operation.Name; + // System database protection: Block deletion of GaussDB core system databases + var systemDatabases = new[] { "postgres", "template0", "template1" }; + if (systemDatabases.Contains(dbName.ToLower(CultureInfo.CurrentCulture))) + { + throw new InvalidOperationException($"Cannot delete GaussDB system database '{dbName}'. Please use a custom database name."); + } + + // Properly escape database name to avoid SQL injection and special character issues + var delimitedDbName = DelimitIdentifier(dbName); + var escapedDbName = dbName.Replace("'", "''"); + + // Generate GaussDB-compatible SQL (complete the condition to exclude current session) + builder.AppendLine($"REVOKE CONNECT ON DATABASE {delimitedDbName} FROM PUBLIC;"); + // Critical fix: Add pid <> pg_backend_pid() to avoid terminating the current session + builder.AppendLine($"SELECT pg_terminate_backend(pid) FROM pg_stat_activity " + + $"WHERE datname = '{escapedDbName}' AND pid <> pg_backend_pid();"); + builder.EndCommand(suppressTransaction: true); + // Add IF EXISTS to avoid errors when the database does not exist + builder.AppendLine($"DROP DATABASE IF EXISTS {delimitedDbName};"); + + EndStatement(builder, suppressTransaction: true); + } + + /// + protected override void Generate( + AlterDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation.Collation != operation.OldDatabase.Collation) + { + throw new NotSupportedException("GaussDB does not support altering the collation on an existing database."); + } + + GenerateCollationStatements(operation, model, builder); + GenerateEnumStatements(operation, model, builder); + GenerateRangeStatements(operation, model, builder); + + foreach (var extension in operation.GetPostgresExtensions()) + { + GenerateCreateExtension(extension, model, builder); + } + + builder.EndCommand(); + } + + /// + protected virtual void GenerateCreateExtension( + GaussDBExtension extension, + IModel? model, + MigrationCommandListBuilder builder) + { + var schema = extension.Schema ?? model?.GetDefaultSchema(); + + // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences + // and other database objects. However, it isn't aware of extensions, so we always ensure schema on enum creation. + if (schema is not null) + { + Generate(new EnsureSchemaOperation { Name = schema }, model, builder); + } + + builder + .Append("CREATE EXTENSION IF NOT EXISTS ") + .Append(DelimitIdentifier(extension.Name)); + + if (extension.Schema is not null) + { + builder + .Append(" SCHEMA ") + .Append(DelimitIdentifier(extension.Schema)); + } + + if (extension.Version is not null) + { + builder + .Append(" VERSION ") + .Append(DelimitIdentifier(extension.Version)); + } + + builder.AppendLine(";"); + } + + #region Collation management + + /// + protected virtual void GenerateCollationStatements(AlterDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + foreach (var collationToCreate in operation.GetPostgresCollations() + .Where(ne => operation.GetOldPostgresCollations().All(oe => oe.Name != ne.Name || oe.Schema != ne.Schema))) + { + GenerateCreateCollation(collationToCreate, model, builder); + } + + foreach (var collationToDrop in operation.GetOldPostgresCollations() + .Where(oe => operation.GetPostgresCollations().All(ne => ne.Name != oe.Name || oe.Schema != ne.Schema))) + { + GenerateDropCollation(collationToDrop, model, builder); + } + + foreach (var (newCollation, oldCollation) in operation.GetPostgresCollations() + .Join( + operation.GetOldPostgresCollations(), + e => new { e.Name, e.Schema }, + e => new { e.Name, e.Schema }, + (ne, oe) => (New: ne, Old: oe))) + { + if (newCollation.LcCollate != oldCollation.LcCollate + || newCollation.LcCtype != oldCollation.LcCtype + || newCollation.Provider != oldCollation.Provider + || newCollation.IsDeterministic != oldCollation.IsDeterministic) + { + throw new NotSupportedException("Altering an existing collation is not supported."); + } + } + } + + /// + protected virtual void GenerateCreateCollation(GaussDBCollation collation, IModel? model, MigrationCommandListBuilder builder) + { + var schema = collation.Schema ?? model?.GetDefaultSchema(); + + // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences + // and other database objects. However, it isn't aware of collation, so we always ensure schema on collation creation. + if (schema is not null) + { + Generate(new EnsureSchemaOperation { Name = schema }, model, builder); + } + + builder + .Append("CREATE COLLATION ") + .Append(DelimitIdentifier(collation.Name, schema)) + .Append(" (") + .IncrementIndent(); + + var def = new List(); + + if (collation.LcCollate == collation.LcCtype) + { + def.Add($"LOCALE = {_stringTypeMapping.GenerateSqlLiteral(collation.LcCollate)}"); + } + else + { + def.Add($"LC_COLLATE = {_stringTypeMapping.GenerateSqlLiteral(collation.LcCollate)}"); + def.Add($"LC_CTYPE = {_stringTypeMapping.GenerateSqlLiteral(collation.LcCtype)}"); + } + + if (collation.Provider is not null) + { + def.Add($"PROVIDER = {collation.Provider}"); + } + + if (collation.IsDeterministic is not null) + { + def.Add($"DETERMINISTIC = {collation.IsDeterministic}"); + } + + for (var i = 0; i < def.Count; i++) + { + builder + .Append(def[i] + (i == def.Count - 1 ? null : ",")) + .AppendLine(); + } + + builder + .DecrementIndent() + .AppendLine(");"); + } + + /// + protected virtual void GenerateDropCollation(GaussDBCollation collation, IModel? model, MigrationCommandListBuilder builder) + { + var schema = collation.Schema ?? model?.GetDefaultSchema(); + + builder + .Append("DROP COLLATION ") + .Append(DelimitIdentifier(collation.Name, schema)) + .AppendLine(";"); + } + + #endregion Collation management + + #region Enum management + + /// + protected virtual void GenerateEnumStatements(AlterDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + foreach (var enumTypeToCreate in operation.GetPostgresEnums() + .Where(ne => operation.GetOldPostgresEnums().All(oe => oe.Name != ne.Name || oe.Schema != ne.Schema))) + { + GenerateCreateEnum(enumTypeToCreate, model, builder); + } + + foreach (var enumTypeToDrop in operation.GetOldPostgresEnums() + .Where(oe => operation.GetPostgresEnums().All(ne => ne.Name != oe.Name || oe.Schema != ne.Schema))) + { + GenerateDropEnum(enumTypeToDrop, model, builder); + } + + foreach (var (newEnum, oldEnum) in operation.GetPostgresEnums().OrderBy(e => e.Schema).ThenBy(e => e.Name) + .Join( + 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))) + { + var (oldLabels, newLabels) = (oldEnum.Labels, newEnum.Labels); + + // We only support adding enum values - dropping is unsupported by GaussDB, and we don't want to + // go into rename detection heuristics (users can do that in raw SQL). + // See https://www.postgresql.org/docs/current/sql-altertype.html + + if (oldLabels.Except(newLabels).FirstOrDefault() is { } removedLabel) + { + throw new NotSupportedException( + $"Can't remove enum label '{removedLabel}' from enum type '{newEnum}'. " + + "Renaming a label is possible via a raw SQL migration (see " + + "https://www.postgresql.org/docs/current/sql-altertype.html)"); + } + + for (var newPos = 0; newPos < newLabels.Count; newPos++) + { + var newLabel = newLabels[newPos]; + if (oldLabels.Contains(newLabel)) + { + continue; + } + + // 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); + } + } + } + } + + /// + protected virtual void GenerateCreateEnum(GaussDBEnum enumType, IModel? model, MigrationCommandListBuilder builder) + { + var schema = enumType.Schema ?? model?.GetDefaultSchema(); + + // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences + // and other database objects. However, it isn't aware of enums, so we always ensure schema on enum creation. + if (schema is not null) + { + Generate(new EnsureSchemaOperation { Name = schema }, model, builder); + } + + builder + .Append("CREATE TYPE ") + .Append(DelimitIdentifier(enumType.Name, schema)) + .Append(" AS ENUM ("); + + var labels = enumType.Labels; + for (var i = 0; i < labels.Count; i++) + { + builder.Append(_stringTypeMapping.GenerateSqlLiteral(labels[i])); + if (i < labels.Count - 1) + { + builder.Append(", "); + } + } + + builder.AppendLine(");"); + } + + /// + protected virtual void GenerateDropEnum(GaussDBEnum enumType, IModel? model, MigrationCommandListBuilder builder) + { + var schema = enumType.Schema ?? model?.GetDefaultSchema(); + + builder + .Append("DROP TYPE ") + .Append(DelimitIdentifier(enumType.Name, schema)) + .AppendLine(";"); + } + + /// + protected virtual void GenerateAddEnumLabel( + GaussDBEnum 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 + .Append("ALTER TYPE ") + .Append(DelimitIdentifier(enumType.Name, schema)) + .Append(" ADD VALUE ") + .Append(_stringTypeMapping.GenerateSqlLiteral(addedLabel)); + + if (beforeLabel is not null) + { + builder + .Append(" BEFORE ") + .Append(_stringTypeMapping.GenerateSqlLiteral(beforeLabel)); + } + else if (afterLabel is not null) + { + builder + .Append(" AFTER ") + .Append(_stringTypeMapping.GenerateSqlLiteral(afterLabel)); + } + + builder.AppendLine(";"); + + // Adding an enum label cannot be done in a transaction prior to PG12 + if (_postgresVersion.IsUnder(12)) + { + EndStatement(builder, suppressTransaction: true); + } + } + + #endregion Enum management + + #region Range management + + /// + protected virtual void GenerateRangeStatements(AlterDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + foreach (var rangeTypeToCreate in operation.GetPostgresRanges() + .Where(ne => operation.GetOldPostgresRanges().All(oe => oe.Name != ne.Name))) + { + GenerateCreateRange(rangeTypeToCreate, model, builder); + } + + foreach (var rangeTypeToDrop in operation.GetOldPostgresRanges() + .Where(oe => operation.GetPostgresRanges().All(ne => ne.Name != oe.Name))) + { + GenerateDropRange(rangeTypeToDrop, model, builder); + } + + if (operation.GetPostgresRanges().FirstOrDefault( + nr => + operation.GetOldPostgresRanges().Any(or => or.Name == nr.Name) + ) is { } rangeTypeToAlter) + { + throw new NotSupportedException($"Altering range type ${rangeTypeToAlter} isn't supported."); + } + } + + /// + protected virtual void GenerateCreateRange(Metadata.GaussDBRange rangeType, IModel? model, MigrationCommandListBuilder builder) + { + var schema = rangeType.Schema ?? model?.GetDefaultSchema(); + + // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences + // and other database objects. However, it isn't aware of ranges, so we always ensure schema on range creation. + if (schema is not null) + { + Generate(new EnsureSchemaOperation { Name = schema }, model, builder); + } + + builder + .Append("CREATE TYPE ") + .Append(DelimitIdentifier(rangeType.Name, schema)) + .AppendLine(" AS RANGE (") + .IncrementIndent(); + + var def = new List { $"SUBTYPE = {rangeType.Subtype}" }; + if (rangeType.CanonicalFunction is not null) + { + def.Add($"CANONICAL = {rangeType.CanonicalFunction}"); + } + + if (rangeType.SubtypeOpClass is not null) + { + def.Add($"SUBTYPE_OPCLASS = {rangeType.SubtypeOpClass}"); + } + + if (rangeType.CanonicalFunction is not null) + { + def.Add($"COLLATION = {rangeType.Collation}"); + } + + if (rangeType.SubtypeDiff is not null) + { + def.Add($"SUBTYPE_DIFF = {rangeType.SubtypeDiff}"); + } + + for (var i = 0; i < def.Count; i++) + { + builder + .Append(def[i] + (i == def.Count - 1 ? null : ",")) + .AppendLine(); + } + + builder + .DecrementIndent() + .AppendLine(");"); + } + + /// + protected virtual void GenerateDropRange(Metadata.GaussDBRange rangeType, IModel? model, MigrationCommandListBuilder builder) + { + var schema = rangeType.Schema ?? model?.GetDefaultSchema(); + + builder + .Append("DROP TYPE ") + .Append(DelimitIdentifier(rangeType.Name, schema)) + .AppendLine(";"); + } + + #endregion Range management + + /// + protected override void Generate( + DropIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder + .Append("DROP INDEX ") + .Append(DelimitIdentifier(operation.Name, operation.Schema)); + + if (terminate) + { + builder.AppendLine(";"); + EndStatement(builder); + } + } + + /// + protected override void Generate(RenameColumnOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + builder.Append("ALTER TABLE ") + .Append(DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" RENAME COLUMN ") + .Append(DelimitIdentifier(operation.Name)) + .Append(" TO ") + .Append(DelimitIdentifier(operation.NewName)) + .AppendLine(";"); + + EndStatement(builder); + } + + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + InsertDataOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + var sqlBuilder = new StringBuilder(); + foreach (var modificationCommand in GenerateModificationCommands(operation, model)) + { + var overridingSystemValue = modificationCommand.ColumnModifications.Any( + m => + m.Property?.GetValueGenerationStrategy() == GaussDBValueGenerationStrategy.IdentityAlwaysColumn); + ((GaussDBUpdateSqlGenerator)Dependencies.UpdateSqlGenerator).AppendInsertOperation( + sqlBuilder, + modificationCommand, + 0, + overridingSystemValue, + out _); + } + + builder.Append(sqlBuilder.ToString()); + + if (terminate) + { + builder.EndCommand(); + } + } + + /// + protected override void Generate(CreateSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + Check.NotNull(operation, nameof(operation)); + + if (_postgresVersion.AtLeast(10)) + { + base.Generate(operation, model, builder); + } + else + { + // "CREATE SEQUENCE name AS type" expression is supported only in GaussDB 10 or above. + // The base MigrationsSqlGenerator.Generate method generates that expression. + // https://github.com/aspnet/EntityFrameworkCore/blob/master/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs#L533-L535 + var oldValue = operation.ClrType; + operation.ClrType = typeof(long); + base.Generate(operation, model, builder); + operation.ClrType = oldValue; + } + } + + #endregion Standard migrations + + #region Utilities + + /// + protected override void ColumnDefinition( + string? schema, + string table, + string name, + ColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (operation.ColumnType is null) + { + operation.ColumnType = GetColumnType(schema, table, name, operation, model); + } + + CheckForOldValueGenerationAnnotation(operation); + var valueGenerationStrategy = operation[GaussDBAnnotationNames.ValueGenerationStrategy] as GaussDBValueGenerationStrategy?; + if (valueGenerationStrategy.IsIdentity() || valueGenerationStrategy == GaussDBValueGenerationStrategy.SerialColumn) + { + if (operation.IsNullable) + { + throw new NotSupportedException("SERIAL columns can't be nullable"); + } + + switch (operation.ColumnType) + { + case "int": + case "int4": + case "integer": + operation.ColumnType = "serial"; + break; + case "bigint": + case "int8": + operation.ColumnType = "bigserial"; + break; + case "smallint": + case "int2": + operation.ColumnType = "smallserial"; + break; + } + valueGenerationStrategy = GaussDBValueGenerationStrategy.SerialColumn; + } + + ApplyTsVectorColumnSql(operation, model, operation.Name, schema, table); + + if (operation.ComputedColumnSql is not null) + { + ComputedColumnDefinition(schema, table, name, operation, model, builder); + + return; + } + + var columnType = operation.ColumnType ?? GetColumnType(schema, table, name, operation, model)!; + builder + .Append(DelimitIdentifier(name)) + .Append(" ") + .Append(columnType); + + if (operation[GaussDBAnnotationNames.CompressionMethod] is string compressionMethod) + { + builder + .Append(" COMPRESSION ") + .Append(DelimitIdentifier(compressionMethod)); + } + + // If a collation was defined on the column specifically, via the standard EF mechanism, it will be + // available in operation.Collation (as usual). + // If not, there may be a model-wide default column collation, which gets transmitted via the GaussDB-specific annotation. + // 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 + var collation = (string?)(operation.Collation ?? operation[GaussDBAnnotationNames.DefaultColumnCollation]); +#pragma warning restore CS0618 + if (collation is not null) + { + builder + .Append(" COLLATE ") + .Append(DelimitIdentifier(collation)); + } + + if (valueGenerationStrategy == GaussDBValueGenerationStrategy.SerialColumn) + { + // Serial ǿƷǿգ IdentityDefinition + builder.Append(" NOT NULL"); + } + else + { + if (!operation.IsNullable) + { + builder.Append(" NOT NULL"); + } + + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder); + } + } + + /// + protected override void DefaultValue( + object? defaultValue, + string? defaultValueSql, + string? columnType, + MigrationCommandListBuilder builder) + { + // This is a hacky workaround for https://github.com/dotnet/efcore/issues/32353 - the EF MigrationsModelDiffer generates an empty + // string as the default value for JSON columns, but that's not valid as a JSON document and rejected by PG's jsonb type. So we + // replace the empty string with an empty JSON document {}. + // Note that even after the EF-side issue is fixed, removing this hack is a breaking change as migrations have already been + // scaffolded with an empty string. + if (columnType is "jsonb" or "json" && defaultValue is "") + { + defaultValue = "{}"; + } + + base.DefaultValue(defaultValue, defaultValueSql, columnType, builder); + } + + /// + /// Checks for a annotation on the given column, and if found, assigns + /// the appropriate SQL to . + /// + protected virtual void ApplyTsVectorColumnSql(ColumnOperation column, IModel? model, string name, string? schema, string table) + { + if (column[GaussDBAnnotationNames.TsVectorConfig] is string tsVectorConfig) + { + var tsVectorIncludedColumns = column[GaussDBAnnotationNames.TsVectorProperties] as string[]; + if (tsVectorIncludedColumns is null) + { + throw new InvalidOperationException( + $"{nameof(GaussDBAnnotationNames.TsVectorConfig)} is present in a migration but " + + $"{nameof(GaussDBAnnotationNames.TsVectorProperties)} is absent or empty"); + } + + column.ComputedColumnSql = ColumnsToTsVector(name, tsVectorIncludedColumns, tsVectorConfig, model, schema, table); + column.IsStored = true; + + column.RemoveAnnotation(GaussDBAnnotationNames.TsVectorConfig); + } + } + + // Note: this definition is only used for creating new identity columns, not for alterations. + /// + protected virtual void IdentityDefinition( + ColumnOperation operation, + MigrationCommandListBuilder builder) + { + if (operation[GaussDBAnnotationNames.ValueGenerationStrategy] is not GaussDBValueGenerationStrategy strategy + || !strategy.IsIdentity()) + { + return; + } + + builder.Append(" serial"); + + // Handle sequence options for the identity column + if (operation[GaussDBAnnotationNames.IdentityOptions] is string identitySequenceOptions) + { + // TODO: Potential for refactoring with regular sequences (i.e. calling SequenceOptions), + // but some complexity to be worked out around creating/altering/restarting + + var sequenceData = IdentitySequenceOptionsData.Deserialize(identitySequenceOptions); + + var optionsWritten = false; + + var incrementBy = sequenceData.IncrementBy; + + var defaultMinValue = incrementBy > 0 ? 1 : Min(operation.ClrType); + var defaultMaxValue = incrementBy > 0 ? Max(operation.ClrType) : -1; + + var minValue = sequenceData.MinValue ?? defaultMinValue; + var maxValue = sequenceData.MaxValue ?? defaultMaxValue; + + var defaultStartValue = incrementBy > 0 ? minValue : maxValue; + if (sequenceData.StartValue.HasValue && sequenceData.StartValue != defaultStartValue) + { + Append("START WITH " + sequenceData.StartValue); + } + + if (incrementBy != 1) + { + Append("INCREMENT BY " + incrementBy); + } + + if (minValue != defaultMinValue) + { + Append("MINVALUE " + minValue); + } + + if (maxValue != defaultMaxValue) + { + Append("MAXVALUE " + maxValue); + } + + if (sequenceData.IsCyclic) + { + Append("CYCLE"); + } + + if (sequenceData.NumbersToCache != 1) + { + Append("CACHE " + sequenceData.NumbersToCache); + } + + if (optionsWritten) + { + builder.Append(")"); + } + + void Append(string s) + { + builder + .Append(optionsWritten ? " " : " (") + .Append(s); + optionsWritten = true; + } + + // Note: in older versions of GaussDB there's a slight variation, see GaussDBDatabaseModelFactory. + // This is currently only used by identity, which is only supported on PG 10 anyway. + long Min(Type type) + { + if (type == typeof(int)) + { + return int.MinValue; + } + + if (type == typeof(long)) + { + return long.MinValue; + } + + if (type == typeof(short)) + { + return short.MinValue; + } + + throw new ArgumentOutOfRangeException(); + } + + long Max(Type type) + { + if (type == typeof(int)) + { + return int.MaxValue; + } + + if (type == typeof(long)) + { + return long.MaxValue; + } + + if (type == typeof(short)) + { + return short.MaxValue; + } + + throw new ArgumentOutOfRangeException(); + } + } + } + + /// + /// Generates a SQL fragment for a computed column definition for the given column metadata. + /// + /// The schema that contains the table, or null to use the default schema. + /// The table that contains the column. + /// The column name. + /// The column metadata. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected override void ComputedColumnDefinition( + string? schema, + string table, + string name, + ColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + + if (_postgresVersion < new Version(12, 0)) + { + throw new NotSupportedException("Computed/generated columns aren't supported in GaussDB prior to version 12"); + } + + if (operation.IsStored is not true && _postgresVersion < new Version(18, 0)) + { + throw new NotSupportedException( + "Virtual (non-stored) generated columns are only supported on GaussDB 18 and up. " + + "On older versions, specify 'stored: true' in " + + $"'{nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql)}' in your context's OnModelCreating."); + } + + builder + .Append(DelimitIdentifier(name)) + .Append(" ") + .Append(operation.ColumnType ?? GetColumnType(schema, table, name, operation, model)!); + + if (operation.Collation is not null) + { + builder + .Append(" COLLATE ") + .Append(DelimitIdentifier(operation.Collation)); + } + + builder + .Append(" GENERATED ALWAYS AS (") + .Append(operation.ComputedColumnSql!) + .Append(")"); + + if (operation.IsStored is true) + { + builder.Append(" STORED"); + } + + if (!operation.IsNullable) + { + builder.Append(" NOT NULL"); + } + } + +#pragma warning disable 618 + // Version 1.0 had a bad strategy for expressing serial columns, which depended on a + // ValueGeneratedOnAdd annotation. Detect that and throw. + private static void CheckForOldValueGenerationAnnotation(IAnnotatable annotatable) + { + if (annotatable.FindAnnotation(GaussDBAnnotationNames.ValueGeneratedOnAdd) is not null) + { + throw new NotSupportedException( + "The GaussDB:ValueGeneratedOnAdd annotation has been found in your migrations, but is no longer supported. Please replace it with '.Annotation(\"GaussDB:ValueGenerationStrategy\", GaussDBValueGenerationStrategy.SerialColumn)' where you want GaussDB serial (autoincrement) columns, and remove it in all other cases."); + } + } +#pragma warning restore 618 + + /// + /// Renames a database object such as a table, index, or sequence. + /// + /// The current schema of the object to rename. + /// The current name of the object to rename. + /// The new name. + /// The type of the object (e.g. TABLE, INDEX, SEQUENCE). + /// The builder to which operations are appended. + public virtual void Rename( + string? schema, + string name, + string newName, + string type, + MigrationCommandListBuilder builder) + { + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(newName, nameof(newName)); + Check.NotEmpty(type, nameof(type)); + Check.NotNull(builder, nameof(builder)); + + builder + .Append("ALTER ") + .Append(type) + .Append(" ") + .Append(DelimitIdentifier(name, schema)) + .Append(" RENAME TO ") + .Append(DelimitIdentifier(newName)) + .AppendLine(";"); + } + + /// + /// Transfers a database object such as a table, index, or sequence between schemas. + /// + /// The new schema. + /// The current schema. + /// The name of the object to transfer. + /// The type of the object (e.g. TABLE, INDEX, SEQUENCE). + /// The builder to which operations are appended. + public virtual void Transfer( + string newSchema, + string? schema, + string name, + string type, + MigrationCommandListBuilder builder) + { + Check.NotEmpty(newSchema, nameof(newSchema)); + Check.NotEmpty(name, nameof(name)); + Check.NotNull(type, nameof(type)); + Check.NotNull(builder, nameof(builder)); + + builder + .Append("ALTER ") + .Append(type) + .Append(" ") + .Append(DelimitIdentifier(name, schema)) + .Append(" SET SCHEMA ") + .Append(DelimitIdentifier(newSchema)) + .AppendLine(";"); + } + + /// + protected virtual void RecreateIndexes(IColumn? column, MigrationOperation currentOperation, MigrationCommandListBuilder builder) + { + foreach (var index in GetIndexesToRebuild()) + { + Generate(CreateIndexOperation.CreateFrom(index), index.Table.Model.Model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + IEnumerable GetIndexesToRebuild() + { + if (column == null) + { + yield break; + } + + var table = column.Table; + var createIndexOperations = _operations.SkipWhile(o => o != currentOperation).Skip(1) + .OfType().Where(o => o.Table == table.Name && o.Schema == table.Schema).ToList(); + foreach (var index in table.Indexes) + { + var indexName = index.Name; + if (createIndexOperations.Any(o => o.Name == indexName)) + { + continue; + } + + if (index.Columns.Any(c => c == column)) + { + yield return index; + } + else if (index[GaussDBAnnotationNames.IndexInclude] is IReadOnlyList includeColumns + && includeColumns.Contains(column.Name)) + { + yield return index; + } + } + } + } + + #endregion Utilities + + #region System column utilities + + private bool IsSystemColumn(string name) + => name == "oid" && _postgresVersion.IsUnder(12) || SystemColumnNames.Contains(name); + + /// + /// Tables in GaussDB implicitly have a set of system columns, which are always there. + /// We want to allow users to access these columns (i.e. xmin for optimistic concurrency) but + /// they should never generate migration operations. + /// + /// + /// https://www.postgresql.org/docs/current/static/ddl-system-columns.html + /// + private static readonly string[] SystemColumnNames = ["tableoid", "xmin", "cmin", "xmax", "cmax", "ctid"]; + + #endregion System column utilities + + #region Storage parameter utilities + + private void AppendStoreParameters(Annotatable annotatable, MigrationCommandListBuilder builder, bool withLeadingNewline) + { + var storageParameters = GetStorageParameters(annotatable); + if (storageParameters.Count > 0) + { + if (withLeadingNewline) + { + builder.AppendLine(); + } + else + { + builder.Append(" "); + } + + builder + .Append("WITH (") + .Append(string.Join(", ", storageParameters.Select(p => $"{p.Key}={p.Value}"))) + .Append(")"); + } + } + + private Dictionary GetStorageParameters(Annotatable annotatable) + => annotatable.GetAnnotations() + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) + .ToDictionary( + a => a.Name.Substring(GaussDBAnnotationNames.StorageParameterPrefix.Length), + a => a.Value switch + { + bool b => b ? "true" : "false", + string s => $"'{s}'", + _ => a.Value!.ToString()! + }); + + // TODO: Call this for AlterIndexOperation when that's added (https://github.com/dotnet/efcore/issues/20692) + private bool AppendStorageParameterAlterations( + Annotatable oldAnnotatable, + Annotatable newAnnotatable, + string alterBaseSql, + MigrationCommandListBuilder builder) + { + var madeChanges = false; + + var oldStorageParameters = GetStorageParameters(oldAnnotatable); + var newStorageParameters = GetStorageParameters(newAnnotatable); + + var newOrChanged = newStorageParameters.Where( + p => + !oldStorageParameters.ContainsKey(p.Key) || oldStorageParameters[p.Key] != p.Value) + .ToList(); + + if (newOrChanged.Count > 0) + { + builder + .Append(alterBaseSql) + .Append(" SET (") + .Append(string.Join(", ", newOrChanged.Select(p => $"{p.Key}={p.Value}"))) + .Append(")"); + + builder.AppendLine(";"); + madeChanges = true; + } + + var removed = oldStorageParameters + .Select(p => p.Key) + .Where(pn => !newStorageParameters.ContainsKey(pn)) + .ToList(); + + if (removed.Count > 0) + { + builder + .Append(alterBaseSql) + .Append(" RESET (") + .Append(string.Join(", ", removed)) + .Append(")"); + + builder.AppendLine(";"); + madeChanges = true; + } + + return madeChanges; + } + + #endregion Storage parameter utilities + + #region Helpers + + private string DelimitIdentifier(string identifier) + => Dependencies.SqlGenerationHelper.DelimitIdentifier(identifier); + + private string DelimitIdentifier(string name, string? schema) + => Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema); + + private string IndexColumnList(IndexColumn[] columns, string? method) + { + var builder = new StringBuilder(); + + for (var i = 0; i < columns.Length; i++) + { + var column = columns[i]; + + if (i > 0) + { + builder.Append(", "); + } + + 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) + ? DelimitIdentifier(name, schema) + : DelimitIdentifier(column.Operator); + + builder.Append(" ").Append(delimitedOperator); + } + + // 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") + { + if (column.IsDescending) + { + builder.Append(" DESC"); + } + + if (column.NullSortOrder != NullSortOrder.Unspecified) + { + builder.Append(" NULLS "); + + switch (column.NullSortOrder) + { + case NullSortOrder.NullsFirst: + builder.Append("FIRST"); + break; + case NullSortOrder.NullsLast: + builder.Append("LAST"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + return builder.ToString(); + } + + private string ColumnsToTsVector( + string columnOrIndexName, + IEnumerable columnNames, + string tsVectorConfig, + IModel? model, + string? schema, + string table) + { + var columns = columnNames + .Select(columnName => model?.GetRelationalModel().FindTable(table, schema)?.Columns.FirstOrDefault(c => c.Name == columnName)) + .ToArray(); + + IEnumerable> columnGroups = columns + .GroupBy( + c => c?.StoreType switch + { + "json" => "json", + "jsonb" => "jsonb", + null => "null", + _ => "text" + + // Note: we currently don't support array_to_tsvector since it doesn't accept a search configuration + })!; + + var tsVectorConfigLiteral = _stringTypeMapping.GenerateSqlLiteral(tsVectorConfig); + + var builder = new StringBuilder(); + + foreach (var columnGroup in columnGroups) + { + if (builder.Length > 0) + { + builder.Append(" || "); + } + + builder.Append( + columnGroup.Key switch + { + "text" => $"to_tsvector({tsVectorConfigLiteral}, {string.Join(" || ' ' || ", columnGroup.Select(TextColumn))})", + "json" => string.Join( + " || ", columnGroup.Select( + c => + $"""json_to_tsvector({tsVectorConfigLiteral}, {JsonColumn(c)}, '"all"')""")), + "jsonb" => string.Join( + " || ", columnGroup.Select( + c => + $"""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() + }); + } + + return builder.ToString(); + + string TextColumn(IColumn column) + => column.IsNullable ? $"coalesce({DelimitIdentifier(column.Name)}, '')" : DelimitIdentifier(column.Name); + + string JsonColumn(IColumn column) + => column.IsNullable ? $"coalesce({DelimitIdentifier(column.Name)}, '{{}}')" : DelimitIdentifier(column.Name); + } + + private static bool TryParseSchema(string identifier, out string name, out string? schema) + { + var index = identifier.IndexOf('.'); + + if (index >= 0) + { + schema = identifier.Substring(0, index); + name = identifier.Substring(index + 1); + return true; + } + + name = identifier; + schema = default; + return false; + } + + private static IndexColumn[] GetIndexColumns(CreateIndexOperation operation) + { + var collations = operation[RelationalAnnotationNames.Collation] as string[]; + + var operators = operation[GaussDBAnnotationNames.IndexOperators] as string[]; + + // We used to have our own annotation-based descending index mechanism, this got replaced with IsDescending in EF Core 7.0. + var isDescendingValues = operation.IsDescending; + var legacySortOrders = operation[GaussDBAnnotationNames.IndexSortOrder] as SortOrder[]; + + var nullSortOrders = operation[GaussDBAnnotationNames.IndexNullSortOrder] as NullSortOrder[]; + + var columns = new IndexColumn[operation.Columns.Length]; + + for (var i = 0; i < columns.Length; i++) + { + var name = operation.Columns[i]; + var @operator = i < operators?.Length ? operators[i] : null; + var collation = i < collations?.Length ? collations[i] : null; + var isColumnDescending = isDescendingValues is not null + ? isDescendingValues.Length == 0 || isDescendingValues[i] + : i < legacySortOrders?.Length && legacySortOrders[i] == SortOrder.Descending; + var nullSortOrder = i < nullSortOrders?.Length ? nullSortOrders[i] : NullSortOrder.Unspecified; + + columns[i] = new IndexColumn(name, @operator, collation, isColumnDescending, nullSortOrder); + } + + return columns; + } + + private readonly struct IndexColumn(string name, string? @operator, string? collation, bool isDescending, NullSortOrder nullSortOrder) + { + 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.GaussDB/Migrations/Internal/GaussDBHistoryRepository.cs b/src/EFCore.GaussDB/Migrations/Internal/GaussDBHistoryRepository.cs new file mode 100644 index 0000000000..3600f56d94 --- /dev/null +++ b/src/EFCore.GaussDB/Migrations/Internal/GaussDBHistoryRepository.cs @@ -0,0 +1,303 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.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 GaussDBHistoryRepository : 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 + /// 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 GaussDBHistoryRepository(HistoryRepositoryDependencies dependencies) + : base(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 + /// 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 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() + { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + + Dependencies.RawSqlCommandBuilder + .Build($"LOCK TABLE {Dependencies.SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema)} IN ACCESS EXCLUSIVE MODE") + .ExecuteNonQuery(CreateRelationalCommandParameters()); + + return new GaussDBMigrationDatabaseLock(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 GaussDBMigrationDatabaseLock(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 + /// 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 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(GaussDBModelFinalizingConvention)); + + 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() + }]); + + /// + /// 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 string GetCreateIfNotExistsScript() + { + var script = GetCreateScript(); + return script.Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); + } + + /// + /// 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 string GetBeginIfNotExistsScript(string migrationId) + => $""" + +DO $EF$ +BEGIN + 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 + /// 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 string GetBeginIfExistsScript(string migrationId) + => $""" +DO $EF$ +BEGIN + 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 + /// 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 string GetEndIfScript() + => """ + 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 GaussDB - 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 GaussDB - 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 GaussDBMigrationDatabaseLock(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.GaussDB/Migrations/Internal/GaussDBMigrator.cs b/src/EFCore.GaussDB/Migrations/Internal/GaussDBMigrator.cs new file mode 100644 index 0000000000..7458bd54eb --- /dev/null +++ b/src/EFCore.GaussDB/Migrations/Internal/GaussDBMigrator.cs @@ -0,0 +1,137 @@ +using Microsoft.EntityFrameworkCore.Migrations.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Internal; + +// Migrator is EF-pubternal, but overriding it is the only way to force GaussDB to ReloadTypes() after executing a migration which adds +// types to the database +#pragma warning disable EF1001 + +/// +/// 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 GaussDBMigrator : Migrator +{ + private readonly IHistoryRepository _historyRepository; + private readonly IRelationalConnection _connection; + + /// + /// 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 GaussDBMigrator( + IMigrationsAssembly migrationsAssembly, + IHistoryRepository historyRepository, + IDatabaseCreator databaseCreator, + IMigrationsSqlGenerator migrationsSqlGenerator, + IRawSqlCommandBuilder rawSqlCommandBuilder, + IMigrationCommandExecutor migrationCommandExecutor, + IRelationalConnection connection, + ISqlGenerationHelper sqlGenerationHelper, + ICurrentDbContext currentContext, + IModelRuntimeInitializer modelRuntimeInitializer, + IDiagnosticsLogger logger, + IRelationalCommandDiagnosticsLogger commandLogger, + IDatabaseProvider databaseProvider, + IMigrationsModelDiffer migrationsModelDiffer, + IDesignTimeModel designTimeModel, + IDbContextOptions contextOptions, + IExecutionStrategy executionStrategy) + : base(migrationsAssembly, historyRepository, databaseCreator, migrationsSqlGenerator, rawSqlCommandBuilder, + migrationCommandExecutor, connection, sqlGenerationHelper, currentContext, modelRuntimeInitializer, logger, + commandLogger, databaseProvider, migrationsModelDiffer, designTimeModel, contextOptions, executionStrategy) + { + _historyRepository = historyRepository; + _connection = connection; + } + + /// + /// 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 Migrate(string? targetMigration) + { + var appliedMigrations = _historyRepository.GetAppliedMigrations(); + + base.Migrate(targetMigration); + + PopulateMigrations( + appliedMigrations.Select(t => t.MigrationId), + targetMigration, + out var migratorData); + + if (migratorData.RevertedMigrations.Count + migratorData.AppliedMigrations.Count == 0) + { + return; + } + + // If a GaussDB extension, enum or range was added, we want GaussDB to reload all types at the ADO.NET level. + var migrations = migratorData.AppliedMigrations.Count > 0 ? migratorData.AppliedMigrations : migratorData.RevertedMigrations; + var reloadTypes = migrations + .SelectMany(m => m.UpOperations) + .OfType() + .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); + + if (reloadTypes && _connection.DbConnection is GaussDBConnection npgsqlConnection) + { + _connection.Open(); + try + { + npgsqlConnection.ReloadTypes(); + } + finally + { + _connection.Close(); + } + } + } + + /// + /// 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 MigrateAsync(string? targetMigration, CancellationToken cancellationToken = default) + { + var appliedMigrations = await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false); + + await base.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false); + + PopulateMigrations( + appliedMigrations.Select(t => t.MigrationId), + targetMigration, + out var migratorData); + + if (migratorData.RevertedMigrations.Count + migratorData.AppliedMigrations.Count == 0) + { + return; + } + + // If a GaussDB extension, enum or range was added, we want GaussDB to reload all types at the ADO.NET level. + var migrations = migratorData.AppliedMigrations.Count > 0 ? migratorData.AppliedMigrations : migratorData.RevertedMigrations; + var reloadTypes = migrations + .SelectMany(m => m.UpOperations) + .OfType() + .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); + + if (reloadTypes && _connection.DbConnection is GaussDBConnection npgsqlConnection) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + try + { + await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + await _connection.CloseAsync().ConfigureAwait(false); + } + } + } +} diff --git a/src/EFCore.GaussDB/Migrations/Operations/GaussDBCreateDatabaseOperation.cs b/src/EFCore.GaussDB/Migrations/Operations/GaussDBCreateDatabaseOperation.cs new file mode 100644 index 0000000000..2311780faa --- /dev/null +++ b/src/EFCore.GaussDB/Migrations/Operations/GaussDBCreateDatabaseOperation.cs @@ -0,0 +1,26 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Operations; + +/// +/// A GaussDB-specific to create a database. +/// +/// +/// See Database migrations. +/// +[DebuggerDisplay("CREATE DATABASE {Name}")] +public class GaussDBCreateDatabaseOperation : DatabaseOperation +{ + /// + /// The name of the database. + /// + public virtual string Name { get; set; } = null!; + + /// + /// The GaussDB database to use as a template for the new database to be created. + /// + public virtual string? Template { get; set; } + + /// + /// The GaussDB tablespace in which to create the database. + /// + public virtual string? Tablespace { get; set; } +} diff --git a/src/EFCore.GaussDB/Migrations/Operations/GaussDBDropDatabaseOperation.cs b/src/EFCore.GaussDB/Migrations/Operations/GaussDBDropDatabaseOperation.cs new file mode 100644 index 0000000000..8506d923f9 --- /dev/null +++ b/src/EFCore.GaussDB/Migrations/Operations/GaussDBDropDatabaseOperation.cs @@ -0,0 +1,15 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Operations; + +/// +/// A GaussDB-specific to drop a database. +/// +/// +/// See Database migrations. +/// +public class GaussDBDropDatabaseOperation : MigrationOperation +{ + /// + /// The name of the database. + /// + public virtual string Name { get; set; } = null!; +} diff --git a/src/EFCore.GaussDB/Properties/AssemblyInfo.cs b/src/EFCore.GaussDB/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ea29dff304 --- /dev/null +++ b/src/EFCore.GaussDB/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Runtime.CompilerServices; + +[assembly: DesignTimeProviderServices("HuaweiCloud.EntityFrameworkCore.GaussDB.Design.Internal.GaussDBDesignTimeServices")] + +[assembly: + InternalsVisibleTo( + "HuaweiCloud.EntityFrameworkCore.GaussDB.FunctionalTests, PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100" + + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" + + "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" + + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] + +[assembly: + InternalsVisibleTo( + "HuaweiCloud.EntityFrameworkCore.GaussDB.Tests, PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100" + + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" + + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" + + "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" + + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] diff --git a/src/EFCore.GaussDB/Properties/GaussDBStrings.Designer.cs b/src/EFCore.GaussDB/Properties/GaussDBStrings.Designer.cs new file mode 100644 index 0000000000..15e84c68c4 --- /dev/null +++ b/src/EFCore.GaussDB/Properties/GaussDBStrings.Designer.cs @@ -0,0 +1,607 @@ +// + +#nullable enable + +using System.Resources; +using Microsoft.EntityFrameworkCore.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBStrings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("HuaweiCloud.EntityFrameworkCore.GaussDB.Properties.GaussDBStrings", typeof(GaussDBStrings).Assembly); + + /// + /// ConfigureDataSource() cannot be used when an externally-provided GaussDBDataSource is passed to UseGaussDB(). Either perform all data source configuration on the external GaussDBDataSource, or pass a connection string to UseGaussDB() and specify the data source configuration there. + /// + 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. + /// + public static string DuplicateColumnCompressionMethodMismatch(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table) + => string.Format( + GetString("DuplicateColumnCompressionMethodMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table)), + entityType1, property1, entityType2, property2, columnName, table); + + /// + /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different value generation strategies. + /// + public static string DuplicateColumnNameValueGenerationStrategyMismatch(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table) + => string.Format( + GetString("DuplicateColumnNameValueGenerationStrategyMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table)), + entityType1, property1, entityType2, property2, columnName, table); + + /// + /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different collation configurations. + /// + public static string DuplicateIndexCollationMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName) + => string.Format( + GetString("DuplicateIndexCollationMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), + index1, entityType1, index2, entityType2, table, indexName); + + /// + /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different concurrent creation configurations. + /// + public static string DuplicateIndexConcurrentCreationMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName) + => string.Format( + GetString("DuplicateIndexConcurrentCreationMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), + index1, entityType1, index2, entityType2, table, indexName); + + /// + /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different included columns: {includedColumns1} and {includedColumns2}. + /// + public static string DuplicateIndexIncludedMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName, object? includedColumns1, object? includedColumns2) + => string.Format( + GetString("DuplicateIndexIncludedMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName), nameof(includedColumns1), nameof(includedColumns2)), + index1, entityType1, index2, entityType2, table, indexName, includedColumns1, includedColumns2); + + /// + /// The EF Core 7.0 JSON support isn't currently supported by the GaussDB provider. To map to JSON, see https://www.npgsql.org/efcore/mapping/json.html. + /// + public static string Ef7JsonMappingNotSupported + => GetString("Ef7JsonMappingNotSupported"); + + /// + /// The 'FreeText' method is not supported because the query has switched to client-evaluation. Inspect the log to determine which query expressions are triggering client-evaluation. + /// + public static string FreeTextFunctionOnClient + => GetString("FreeTextFunctionOnClient"); + + /// + /// Heterogeneous store types detected when making new array ({type1}, {type2}). + /// + public static string HeterogeneousTypesInNewArray(object? type1, object? type2) + => string.Format( + GetString("HeterogeneousTypesInNewArray", nameof(type1), nameof(type2)), + type1, type2); + + /// + /// Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with signed integer properties. + /// + public static string IdentityBadType(object? property, object? entityType, object? propertyType) + => string.Format( + GetString("IdentityBadType", nameof(property), nameof(entityType), nameof(propertyType)), + property, entityType, propertyType); + + /// + /// Include property '{entityType}.{property}' cannot be defined multiple times + /// + public static string IncludePropertyDuplicated(object? entityType, object? property) + => string.Format( + GetString("IncludePropertyDuplicated", nameof(entityType), nameof(property)), + entityType, property); + + /// + /// Include property '{entityType}.{property}' is already included in the index + /// + public static string IncludePropertyInIndex(object? entityType, object? property) + => string.Format( + GetString("IncludePropertyInIndex", nameof(entityType), nameof(property)), + entityType, property); + + /// + /// Include property '{entityType}.{property}' not found + /// + public static string IncludePropertyNotFound(object? entityType, object? property) + => string.Format( + GetString("IncludePropertyNotFound", nameof(entityType), nameof(property)), + entityType, property); + + /// + /// The specified table '{table}' is not valid. Specify tables using the format '[schema].[table]'. + /// + public static string InvalidTableToIncludeInScaffolding(object? table) + => string.Format( + GetString("InvalidTableToIncludeInScaffolding", nameof(table)), + table); + + /// + /// The property '{property}' on entity type '{entityType}' is configured to use 'SequenceHiLo' value generator, which is only intended for keys. If this was intentional configure an alternate key on the property, otherwise call 'ValueGeneratedNever' or configure store generation for this property. + /// + public static string NonKeyValueGeneration(object? property, object? entityType) + => string.Format( + GetString("NonKeyValueGeneration", nameof(property), nameof(entityType)), + property, entityType); + + /// + /// Row values comparisons require two tuple arguments of the same length. + /// + public static string RowValueComparisonRequiresTuplesOfSameLength + => GetString("RowValueComparisonRequiresTuplesOfSameLength"); + + /// + /// Cannot set ProvideClientCertificatesCallback, RemoteCertificateValidationCallback or ProvidePasswordCallback when a data source is provided. + /// + public static string CannotUseDataSourceWithAuthCallbacks + => GetString("CannotUseDataSourceWithAuthCallbacks"); + + /// + /// GaussDB sequences cannot be used to generate values for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Sequences can only be used with integer properties. + /// + public static string SequenceBadType(object? property, object? entityType, object? propertyType) + => string.Format( + GetString("SequenceBadType", nameof(property), nameof(entityType), nameof(propertyType)), + property, entityType, propertyType); + + /// + /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. GaussDB stored procedures do not support result columns; use output parameters instead. + /// + public static string StoredProcedureResultColumnsNotSupported(object? entityType, object? sproc) + => string.Format( + GetString("StoredProcedureResultColumnsNotSupported", nameof(entityType), nameof(sproc)), + entityType, sproc); + + /// + /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. GaussDB stored procedures do not support return values; use output parameters instead. + /// + public static string StoredProcedureReturnValueNotSupported(object? entityType, object? sproc) + => string.Format( + 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. + /// + public static string TransientExceptionDetected + => GetString("TransientExceptionDetected"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name)!; + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBResources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("HuaweiCloud.EntityFrameworkCore.GaussDB.Properties.GaussDBStrings", typeof(GaussDBResources).Assembly); + + /// + /// Enum column '{name}' cannot be scaffolded, define a CLR enum type and add the property manually. + /// + public static EventDefinition LogEnumColumnSkipped(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogEnumColumnSkipped; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogEnumColumnSkipped, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.EnumColumnSkippedWarning, + LogLevel.Warning, + "GaussDBEfEventId.EnumColumnSkippedWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.EnumColumnSkippedWarning, + _resourceManager.GetString("LogEnumColumnSkipped")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Expression index '{name}' on table {tableName} cannot be scaffolded, expression indices aren't supported and must be added via raw SQL in migrations. + /// + public static EventDefinition LogExpressionIndexSkipped(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogExpressionIndexSkipped; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogExpressionIndexSkipped, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.ExpressionIndexSkippedWarning, + LogLevel.Warning, + "GaussDBEfEventId.ExpressionIndexSkippedWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.ExpressionIndexSkippedWarning, + _resourceManager.GetString("LogExpressionIndexSkipped")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Found collation with name: {collationName}, schema: {schema}, LC_COLLATE: {lcCollate}, LC_CTYPE: {lcCtype}, provider: {provider}, deterministic: {isDeterministic} + /// + public static EventDefinition LogFoundCollation(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundCollation; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundCollation, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.CollationFound, + LogLevel.Debug, + "GaussDBEfEventId.CollationFound", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.CollationFound, + _resourceManager.GetString("LogFoundCollation")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Found column with table: {tableName}, column name: {columnName}, data type: {dataType}, nullable: {isNullable}, identity: {isIdentity}, default value: {defaultValue}, computed value: {computedValue} + /// + public static FallbackEventDefinition LogFoundColumn(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundColumn; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundColumn, + logger, + static logger => new FallbackEventDefinition( + logger.Options, + GaussDBEfEventId.ColumnFound, + LogLevel.Debug, + "GaussDBEfEventId.ColumnFound", + _resourceManager.GetString("LogFoundColumn")!)); + } + + return (FallbackEventDefinition)definition; + } + + /// + /// Found foreign key on table: {tableName}, name: {foreignKeyName}, principal table: {principalTableName}, delete action: {deleteAction}. + /// + public static EventDefinition LogFoundForeignKey(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundForeignKey; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundForeignKey, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.ForeignKeyFound, + LogLevel.Debug, + "GaussDBEfEventId.ForeignKeyFound", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.ForeignKeyFound, + _resourceManager.GetString("LogFoundForeignKey")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Found index with name: {indexName}, table: {tableName}, is unique: {isUnique}. + /// + public static EventDefinition LogFoundIndex(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundIndex; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundIndex, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.IndexFound, + LogLevel.Debug, + "GaussDBEfEventId.IndexFound", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.IndexFound, + _resourceManager.GetString("LogFoundIndex")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Found primary key with name: {primaryKeyName}, table: {tableName}. + /// + public static EventDefinition LogFoundPrimaryKey(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundPrimaryKey; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundPrimaryKey, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.PrimaryKeyFound, + LogLevel.Debug, + "GaussDBEfEventId.PrimaryKeyFound", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.PrimaryKeyFound, + _resourceManager.GetString("LogFoundPrimaryKey")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Found sequence name: {name}, data type: {dataType}, cyclic: {isCyclic}, increment: {increment}, start: {start}, minimum: {min}, maximum: {max}. + /// + public static FallbackEventDefinition LogFoundSequence(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundSequence; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundSequence, + logger, + static logger => new FallbackEventDefinition( + logger.Options, + GaussDBEfEventId.SequenceFound, + LogLevel.Debug, + "GaussDBEfEventId.SequenceFound", + _resourceManager.GetString("LogFoundSequence")!)); + } + + return (FallbackEventDefinition)definition; + } + + /// + /// Found table with name: {name}. + /// + public static EventDefinition LogFoundTable(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundTable; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundTable, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.TableFound, + LogLevel.Debug, + "GaussDBEfEventId.TableFound", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.TableFound, + _resourceManager.GetString("LogFoundTable")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Found unique constraint with name: {uniqueConstraintName}, table: {tableName}. + /// + public static EventDefinition LogFoundUniqueConstraint(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundUniqueConstraint; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogFoundUniqueConstraint, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.UniqueConstraintFound, + LogLevel.Debug, + "GaussDBEfEventId.UniqueConstraintFound", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.UniqueConstraintFound, + _resourceManager.GetString("LogFoundUniqueConstraint")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Unable to find a schema in the database matching the selected schema {schema}. + /// + public static EventDefinition LogMissingSchema(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogMissingSchema; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogMissingSchema, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.MissingSchemaWarning, + LogLevel.Warning, + "GaussDBEfEventId.MissingSchemaWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.MissingSchemaWarning, + _resourceManager.GetString("LogMissingSchema")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Unable to find a table in the database matching the selected table {table}. + /// + public static EventDefinition LogMissingTable(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogMissingTable; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogMissingTable, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.MissingTableWarning, + LogLevel.Warning, + "GaussDBEfEventId.MissingTableWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.MissingTableWarning, + _resourceManager.GetString("LogMissingTable")!))); + } + + return (EventDefinition)definition; + } + + /// + /// For foreign key {foreignKeyName} on table {tableName}, unable to find the column called {principalColumnName} on the foreign key's principal table, {principaltableName}. Skipping foreign key. + /// + public static EventDefinition LogPrincipalColumnNotFound(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogPrincipalColumnNotFound; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogPrincipalColumnNotFound, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.ForeignKeyPrincipalColumnMissingWarning, + LogLevel.Warning, + "GaussDBEfEventId.ForeignKeyPrincipalColumnMissingWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.ForeignKeyPrincipalColumnMissingWarning, + _resourceManager.GetString("LogPrincipalColumnNotFound")!))); + } + + return (EventDefinition)definition; + } + + /// + /// For foreign key {fkName} on table {tableName}, unable to model the end of the foreign key on principal table {principaltableName}. This is usually because the principal table was not included in the selection set. + /// + public static EventDefinition LogPrincipalTableNotInSelectionSet(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogPrincipalTableNotInSelectionSet; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogPrincipalTableNotInSelectionSet, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.ForeignKeyReferencesMissingPrincipalTableWarning, + LogLevel.Warning, + "GaussDBEfEventId.ForeignKeyReferencesMissingPrincipalTableWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.ForeignKeyReferencesMissingPrincipalTableWarning, + _resourceManager.GetString("LogPrincipalTableNotInSelectionSet")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Constraint '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). + /// + public static EventDefinition LogUnsupportedColumnConstraintSkipped(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogUnsupportedColumnConstraintSkipped; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogUnsupportedColumnConstraintSkipped, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.UnsupportedColumnConstraintSkippedWarning, + LogLevel.Warning, + "GaussDBEfEventId.UnsupportedColumnConstraintSkippedWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.UnsupportedColumnConstraintSkippedWarning, + _resourceManager.GetString("LogUnsupportedColumnConstraintSkipped")!))); + } + + return (EventDefinition)definition; + } + + /// + /// Index '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). + /// + public static EventDefinition LogUnsupportedColumnIndexSkipped(IDiagnosticsLogger logger) + { + var definition = ((GaussDBLoggingDefinitions)logger.Definitions).LogUnsupportedColumnIndexSkipped; + if (definition is null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((GaussDBLoggingDefinitions)logger.Definitions).LogUnsupportedColumnIndexSkipped, + logger, + static logger => new EventDefinition( + logger.Options, + GaussDBEfEventId.UnsupportedColumnIndexSkippedWarning, + LogLevel.Warning, + "GaussDBEfEventId.UnsupportedColumnIndexSkippedWarning", + level => LoggerMessage.Define( + level, + GaussDBEfEventId.UnsupportedColumnIndexSkippedWarning, + _resourceManager.GetString("LogUnsupportedColumnIndexSkipped")!))); + } + + return (EventDefinition)definition; + } + } +} + diff --git a/src/EFCore.GaussDB/Properties/GaussDBStrings.Designer.tt b/src/EFCore.GaussDB/Properties/GaussDBStrings.Designer.tt new file mode 100644 index 0000000000..51ae9d5a40 --- /dev/null +++ b/src/EFCore.GaussDB/Properties/GaussDBStrings.Designer.tt @@ -0,0 +1,5 @@ +<# + Session["ResourceFile"] = "GaussDBStrings.resx"; + Session["LoggingDefinitionsClass"] = "Diagnostics.Internal.GaussDBLoggingDefinitions"; +#> +<#@ include file="..\..\..\tools\Resources.tt" #> diff --git a/src/EFCore.GaussDB/Properties/GaussDBStrings.resx b/src/EFCore.GaussDB/Properties/GaussDBStrings.resx new file mode 100644 index 0000000000..66cf053fa3 --- /dev/null +++ b/src/EFCore.GaussDB/Properties/GaussDBStrings.resx @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ConfigureDataSource() cannot be used when an externally-provided GaussDBDataSource is passed to UseGaussDB(). Either perform all data source configuration on the external GaussDBDataSource, or pass a connection string to UseGaussDB() 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. + + + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different value generation strategies. + + + The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different collation configurations. + + + The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different concurrent creation configurations. + + + The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different included columns: {includedColumns1} and {includedColumns2}. + + + The EF Core 7.0 JSON support isn't currently supported by the GaussDB provider. To map to JSON, see https://www.npgsql.org/efcore/mapping/json.html. + + + The 'FreeText' method is not supported because the query has switched to client-evaluation. Inspect the log to determine which query expressions are triggering client-evaluation. + + + Heterogeneous store types detected when making new array ({type1}, {type2}). + + + Include property '{entityType}.{property}' cannot be defined multiple times + + + Include property '{entityType}.{property}' is already included in the index + + + Include property '{entityType}.{property}' not found + + + The specified table '{table}' is not valid. Specify tables using the format '[schema].[table]'. + + + Enum column '{name}' cannot be scaffolded, define a CLR enum type and add the property manually. + Warning GaussDBEventId.EnumColumnSkippedWarning string + + + Expression index '{name}' on table {tableName} cannot be scaffolded, expression indices aren't supported and must be added via raw SQL in migrations. + Warning GaussDBEventId.ExpressionIndexSkippedWarning string string + + + Found collation with name: {collationName}, schema: {schema}, LC_COLLATE: {lcCollate}, LC_CTYPE: {lcCtype}, provider: {provider}, deterministic: {isDeterministic} + Debug GaussDBEventId.CollationFound string string string string string? bool + + + Found column with table: {tableName}, column name: {columnName}, data type: {dataType}, nullable: {isNullable}, identity: {isIdentity}, default value: {defaultValue}, computed value: {computedValue} + Debug GaussDBEventId.ColumnFound string string string bool bool string string + + + Found foreign key on table: {tableName}, name: {foreignKeyName}, principal table: {principalTableName}, delete action: {deleteAction}. + Debug GaussDBEventId.ForeignKeyFound string string string string + + + Found index with name: {indexName}, table: {tableName}, is unique: {isUnique}. + Debug GaussDBEventId.IndexFound string string bool + + + Found primary key with name: {primaryKeyName}, table: {tableName}. + Debug GaussDBEventId.PrimaryKeyFound string string + + + Found sequence name: {name}, data type: {dataType}, cyclic: {isCyclic}, increment: {increment}, start: {start}, minimum: {min}, maximum: {max}. + Debug GaussDBEventId.SequenceFound string string bool int long long long + + + Found table with name: {name}. + Debug GaussDBEventId.TableFound string + + + Found unique constraint with name: {uniqueConstraintName}, table: {tableName}. + Debug GaussDBEventId.UniqueConstraintFound string? string + + + Unable to find a schema in the database matching the selected schema {schema}. + Warning GaussDBEventId.MissingSchemaWarning string? + + + Unable to find a table in the database matching the selected table {table}. + Warning GaussDBEventId.MissingTableWarning string? + + + For foreign key {foreignKeyName} on table {tableName}, unable to find the column called {principalColumnName} on the foreign key's principal table, {principaltableName}. Skipping foreign key. + Warning GaussDBEventId.ForeignKeyPrincipalColumnMissingWarning string string string string + + + For foreign key {fkName} on table {tableName}, unable to model the end of the foreign key on principal table {principaltableName}. This is usually because the principal table was not included in the selection set. + Warning GaussDBEventId.ForeignKeyReferencesMissingPrincipalTableWarning string? string? string? + + + Constraint '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). + Warning GaussDBEventId.UnsupportedColumnConstraintSkippedWarning string? string + + + Index '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). + Warning GaussDBEventId.UnsupportedColumnIndexSkippedWarning string string + + + The property '{property}' on entity type '{entityType}' is configured to use 'SequenceHiLo' value generator, which is only intended for keys. If this was intentional configure an alternate key on the property, otherwise call 'ValueGeneratedNever' or configure store generation for this property. + + + GaussDB sequences cannot be used to generate values for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Sequences can only be used with integer properties. + + + 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. + + + Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with signed integer properties. + + + Row values comparisons require two tuple arguments of the same length. + + + Cannot set ProvideClientCertificatesCallback, RemoteCertificateValidationCallback or ProvidePasswordCallback when a data source is provided. + + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. GaussDB stored procedures do not support result columns; use output parameters instead. + + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. GaussDB stored procedures do not support return values; use output parameters instead. + + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. GaussDB 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.GaussDB/Query/ExpressionTranslators/Internal/GaussDBAggregateMethodCallTranslatorProvider.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBAggregateMethodCallTranslatorProvider.cs new file mode 100644 index 0000000000..6cc9429777 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBAggregateMethodCallTranslatorProvider.cs @@ -0,0 +1,32 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBAggregateMethodCallTranslatorProvider : RelationalAggregateMethodCallTranslatorProvider +{ + /// + /// 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 GaussDBAggregateMethodCallTranslatorProvider( + RelationalAggregateMethodCallTranslatorProviderDependencies dependencies, + IModel model) + : base(dependencies) + { + var sqlExpressionFactory = (GaussDBExpressionFactory)dependencies.SqlExpressionFactory; + var typeMappingSource = dependencies.RelationalTypeMappingSource; + + AddTranslators( + [ + new GaussDBQueryableAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource), + new GaussDBStatisticsAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource), + new GaussDBMiscAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource, model) + ]); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBArrayMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBArrayMethodTranslator.cs new file mode 100644 index 0000000000..b043631065 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBArrayMethodTranslator.cs @@ -0,0 +1,210 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Translates method and property calls on arrays/lists into their corresponding GaussDB operations. +/// +/// +/// https://www.postgresql.org/docs/current/static/functions-array.html +/// +public class GaussDBArrayMethodTranslator : IMethodCallTranslator +{ + #region Methods + + // ReSharper disable InconsistentNaming + private static readonly MethodInfo Array_IndexOf1 = + typeof(Array).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m is { Name: nameof(Array.IndexOf), IsGenericMethod: true } && m.GetParameters().Length == 2); + + private static readonly MethodInfo Array_IndexOf2 = + typeof(Array).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m is { Name: nameof(Array.IndexOf), IsGenericMethod: true } && m.GetParameters().Length == 3); + + private static readonly MethodInfo Enumerable_ElementAt = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single( + m => m.Name == nameof(Enumerable.ElementAt) + && m.GetParameters().Length == 2 + && m.GetParameters()[1].ParameterType == typeof(int)); + + 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 + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly GaussDBJsonPocoTranslator _jsonPocoTranslator; + + /// + /// 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 GaussDBArrayMethodTranslator(GaussDBExpressionFactory sqlExpressionFactory, GaussDBJsonPocoTranslator jsonPocoTranslator) + { + _sqlExpressionFactory = sqlExpressionFactory; + _jsonPocoTranslator = jsonPocoTranslator; + } + + /// + /// 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? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; so we handle indexing into array/list here + if (method.IsClosedFormOf(Enumerable_ElementAt)) + { + // Indexing over bytea is special, we have to use function rather than subscript + if (arguments[0].TypeMapping is GaussDBByteArrayTypeMapping) + { + return _sqlExpressionFactory.Function( + "get_byte", + [arguments[0], arguments[1]], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(byte)); + } + + // Try translating indexing inside JSON column + // Note that Length over PG arrays (not within JSON) gets translated by QueryableMethodTranslatingEV, since arrays are primitive + // collections + return _jsonPocoTranslator.TranslateMemberAccess(arguments[0], arguments[1], method.ReturnType); + } + + if (method.IsClosedFormOf(Enumerable_SequenceEqual) + && arguments[0].Type.IsArrayOrGenericList() + && !IsMappedToNonArray(arguments[0]) + && arguments[1].Type.IsArrayOrGenericList() + && !IsMappedToNonArray(arguments[1])) + { + return _sqlExpressionFactory.Equal(arguments[0], arguments[1]); + } + + // Translate instance methods on List + if (instance is not null && instance.Type.IsGenericList() && !IsMappedToNonArray(instance)) + { + return TranslateCommon(instance, arguments); + } + + // Translate extension methods over array or List + if (instance is null && arguments.Count > 0 && arguments[0].Type.IsArrayOrGenericList() && !IsMappedToNonArray(arguments[0])) + { + return TranslateCommon(arguments[0], arguments.Slice(1)); + } + + return null; + + // The array/list CLR type may be mapped to a non-array database type (e.g. byte[] to bytea, or just + // value converters) - we don't want to translate for those cases. + static bool IsMappedToNonArray(SqlExpression arrayOrList) + => arrayOrList.TypeMapping is { } and not (GaussDBArrayTypeMapping or GaussDBJsonTypeMapping); + +#pragma warning disable CS8321 + SqlExpression? TranslateCommon(SqlExpression arrayOrList, IReadOnlyList arguments) +#pragma warning restore CS8321 + { + if (method.IsClosedFormOf(Array_IndexOf1) + || method.Name == nameof(List.IndexOf) + && method.DeclaringType.IsGenericList() + && method.GetParameters().Length == 1) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + + return _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Subtract( + _sqlExpressionFactory.Function( + "array_position", + [array, item], + nullable: true, + // 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)); + } + + if (method.IsClosedFormOf(Array_IndexOf2) + || method.Name == nameof(List.IndexOf) + && method.DeclaringType.IsGenericList() + && method.GetParameters().Length == 2) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + var startIndex = _sqlExpressionFactory.GenerateOneBasedIndexExpression(arguments[1]); + + return _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Subtract( + _sqlExpressionFactory.Function( + "array_position", + [array, item, startIndex], + nullable: true, + // 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.GaussDB/Query/ExpressionTranslators/Internal/GaussDBBigIntegerMemberTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBBigIntegerMemberTranslator.cs new file mode 100644 index 0000000000..42dba7cb4b --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBBigIntegerMemberTranslator.cs @@ -0,0 +1,59 @@ +using System.Numerics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBBigIntegerMemberTranslator : IMemberTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + + private static readonly MemberInfo IsZero = typeof(BigInteger).GetProperty(nameof(BigInteger.IsZero))!; + private static readonly MemberInfo IsOne = typeof(BigInteger).GetProperty(nameof(BigInteger.IsOne))!; + private static readonly MemberInfo IsEven = typeof(BigInteger).GetProperty(nameof(BigInteger.IsEven))!; + + /// + /// 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 GaussDBBigIntegerMemberTranslator(GaussDBExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (member.DeclaringType == typeof(BigInteger)) + { + if (member == IsZero) + { + return _sqlExpressionFactory.Equal(instance!, _sqlExpressionFactory.Constant(BigInteger.Zero)); + } + + if (member == IsOne) + { + return _sqlExpressionFactory.Equal(instance!, _sqlExpressionFactory.Constant(BigInteger.One)); + } + + if (member == IsEven) + { + return _sqlExpressionFactory.Equal( + _sqlExpressionFactory.Modulo(instance!, _sqlExpressionFactory.Constant(new BigInteger(2))), + _sqlExpressionFactory.Constant(BigInteger.Zero)); + } + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBByteArrayMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBByteArrayMethodTranslator.cs new file mode 100644 index 0000000000..039d49d311 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBByteArrayMethodTranslator.cs @@ -0,0 +1,97 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBByteArrayMethodTranslator : IMethodCallTranslator +{ + 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 GaussDBByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + Check.NotNull(method, nameof(method)); + Check.NotNull(arguments, nameof(arguments)); + + if (method.IsGenericMethod && arguments[0].TypeMapping is GaussDBByteArrayTypeMapping typeMapping) + { + // Note: we only translate if the array argument is a column mapped to bytea. There are various other + // cases (e.g. Where(b => new byte[] { 1, 2, 3 }.Contains(b.SomeByte))) where we prefer to translate via + // regular GaussDB array logic. + if (method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains)) + { + var source = arguments[0]; + + // We have a byte value, but we need a bytea for GaussDB POSITION. + var value = arguments[1] is SqlConstantExpression constantValue + ? _sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, typeMapping) + // Create bytea from non-constant byte: SELECT set_byte('\x00', 0, 8::smallint); + : _sqlExpressionFactory.Function( + "set_byte", + [ + _sqlExpressionFactory.Constant(new[] { (byte)0 }, typeMapping), + _sqlExpressionFactory.Constant(0), + arguments[1] + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + typeof(byte[]), + typeMapping); + + return _sqlExpressionFactory.GreaterThan( + GaussDBFunctionExpression.CreateWithArgumentSeparators( + "position", + [value, source], + ["IN"], // POSITION(x IN y) + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + builtIn: true, + typeof(int), + null), + _sqlExpressionFactory.Constant(0)); + } + + if (method.GetGenericMethodDefinition().Equals(EnumerableMethods.FirstWithoutPredicate)) + { + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.Function( + "get_byte", + [arguments[0], _sqlExpressionFactory.Constant(0)], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(byte)), + method.ReturnType); + } + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBConvertTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBConvertTranslator.cs new file mode 100644 index 0000000000..5852afa6f1 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBConvertTranslator.cs @@ -0,0 +1,70 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Translates methods defined on into GaussDB CAST expressions. +/// +public class GaussDBConvertTranslator : IMethodCallTranslator +{ + private static readonly Dictionary TypeMapping = new() + { + [nameof(Convert.ToBoolean)] = "bool", + [nameof(Convert.ToByte)] = "smallint", + [nameof(Convert.ToDecimal)] = "numeric", + [nameof(Convert.ToDouble)] = "double precision", + [nameof(Convert.ToInt16)] = "smallint", + [nameof(Convert.ToInt32)] = "int", + [nameof(Convert.ToInt64)] = "bigint", + [nameof(Convert.ToString)] = "text" + }; + + private static readonly List SupportedTypes = + [ + typeof(bool), + typeof(byte), + typeof(decimal), + typeof(double), + typeof(float), + typeof(int), + typeof(long), + typeof(short), + typeof(string), + typeof(object) + ]; + + private static readonly List SupportedMethods + = TypeMapping.Keys + .SelectMany( + t => typeof(Convert).GetTypeInfo().GetDeclaredMethods(t) + .Where( + m => m.GetParameters().Length == 1 + && SupportedTypes.Contains(m.GetParameters().First().ParameterType))) + .ToList(); + + 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 GaussDBConvertTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => SupportedMethods.Contains(method) + ? _sqlExpressionFactory.Convert(arguments[0], method.ReturnType) + : null; +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBDateTimeMemberTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBDateTimeMemberTranslator.cs new file mode 100644 index 0000000000..6cab9e9554 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBDateTimeMemberTranslator.cs @@ -0,0 +1,272 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Provides translation services for members. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-datetime.html +/// +public class GaussDBDateTimeMemberTranslator : IMemberTranslator +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _timestampMapping; + private readonly RelationalTypeMapping _timestampTzMapping; + + /// + /// 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 GaussDBDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSource, GaussDBExpressionFactory sqlExpressionFactory) + { + _typeMappingSource = typeMappingSource; + _timestampMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp without time zone")!; + _timestampTzMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp with time zone")!; + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + var declaringType = member.DeclaringType; + + if (declaringType != typeof(DateTime) + && declaringType != typeof(DateTimeOffset) + && declaringType != typeof(DateOnly) + && declaringType != typeof(TimeOnly)) + { + return null; + } + + if (declaringType == typeof(DateTimeOffset) + && instance is not null + && TranslateDateTimeOffset(instance, member) is { } translated) + { + 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 + // a PG timestamp/timestamptz) rather than a conversion to PG date (compare with NodaTime where we want a LocalDate). + + // When given a timestamptz, date_trunc performs the truncation with respect to TimeZone; to avoid that, we use the overload + // accepting a time zone, and pass UTC. For regular timestamp (or in legacy timestamp mode), we use the simpler overload without + // a time zone. + switch (instance) + { + case { TypeMapping: GaussDBTimestampTypeMapping }: + case { } when GaussDBTypeMappingSource.LegacyTimestampBehavior: + return _sqlExpressionFactory.Function( + "date_trunc", + [_sqlExpressionFactory.Constant("day"), instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + returnType, + instance.TypeMapping); + + case { TypeMapping: GaussDBTimestampTzTypeMapping }: + return _sqlExpressionFactory.Function( + "date_trunc", + [_sqlExpressionFactory.Constant("day"), instance, _sqlExpressionFactory.Constant("UTC")], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + returnType, + instance.TypeMapping); + + // If DateTime.Date is invoked on a GaussDB date (or DateOnly, which can only be mapped to datE), simply no-op. + case { TypeMapping: GaussDBDateTimeDateTypeMapping }: + case { Type: var type } when type == typeof(DateOnly): + return instance; + + default: + return null; + } + } + + return member.Name switch + { + // Legacy behavior + nameof(DateTime.Now) when GaussDBTypeMappingSource.LegacyTimestampBehavior + => UtcNow(), + nameof(DateTime.UtcNow) when GaussDBTypeMappingSource.LegacyTimestampBehavior + => _sqlExpressionFactory.AtUtc(UtcNow()), // Return a UTC timestamp, but as timestamp without time zone + + // We support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a non-UTC + // DateTimeOffset. + nameof(DateTime.Now) => declaringType == typeof(DateTimeOffset) + ? throw new InvalidOperationException("Cannot translate DateTimeOffset.Now - use UtcNow.") + : LocalNow(), + nameof(DateTime.UtcNow) => UtcNow(), + + nameof(DateTime.Today) => _sqlExpressionFactory.Function( + "date_trunc", + [_sqlExpressionFactory.Constant("day"), LocalNow()], + nullable: false, + argumentsPropagateNullability: FalseArrays[2], + typeof(DateTime), + _timestampMapping), + + nameof(DateTime.Year) => DatePart(instance!, "year"), + nameof(DateTime.Month) => DatePart(instance!, "month"), + nameof(DateTime.DayOfYear) => DatePart(instance!, "doy"), + nameof(DateTime.Day) => DatePart(instance!, "day"), + nameof(DateTime.Hour) => DatePart(instance!, "hour"), + nameof(DateTime.Minute) => DatePart(instance!, "minute"), + nameof(DateTime.Second) => DatePart(instance!, "second"), + + nameof(DateTime.Millisecond) => null, // Too annoying + + // .NET's DayOfWeek is an enum, but its int values happen to correspond to GaussDB + nameof(DateTime.DayOfWeek) => DatePart(instance!, "dow", floor: true), + + // Casting a timestamptz to time (to get the time component) converts it to a local timestamp based on TimeZone. + // Convert to a timestamp without time zone at UTC to get the right values. + nameof(DateTime.TimeOfDay) when TryConvertAwayFromTimestampTz(instance!, out var convertedInstance) + => _sqlExpressionFactory.Convert( + convertedInstance, + typeof(TimeSpan), + _typeMappingSource.FindMapping(typeof(TimeSpan), storeTypeName: "time")), + + // TODO: Should be possible + nameof(DateTime.Ticks) => null, + + _ => null + }; + + SqlExpression UtcNow() + => _sqlExpressionFactory.Function( + "now", + [], + nullable: false, + argumentsPropagateNullability: TrueArrays[0], + returnType, + _timestampTzMapping); + + SqlExpression LocalNow() + => _sqlExpressionFactory.Convert(UtcNow(), returnType, _timestampMapping); + } + + private SqlExpression? DatePart( + SqlExpression instance, + string partName, + bool floor = false) + { + // date_part exists only for timestamp without time zone, so if we pass in a timestamptz it gets converted to a local + // timestamp based on TimeZone. Convert to a timestamp without time zone at UTC to get the right values. + if (!TryConvertAwayFromTimestampTz(instance, out instance!)) + { + return null; + } + + var result = _sqlExpressionFactory.Function( + "date_part", + [_sqlExpressionFactory.Constant(partName), instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(double)); + + if (floor) + { + result = _sqlExpressionFactory.Function( + "floor", + [result], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(double)); + } + + return _sqlExpressionFactory.Convert(result, typeof(int)); + } + + 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 + nameof(DateTimeOffset.DateTime) => _sqlExpressionFactory.AtUtc(instance), + + // We only support UTC DateTimeOffset, so DateTimeOffset.UtcDateTime does nothing (type change on CLR change, no change on the + // PG side. + nameof(DateTimeOffset.UtcDateTime) => instance, + + // Convert to timestamp without time zone, applying a time zone conversion based on the TimeZone connection parameter. + nameof(DateTimeOffset.LocalDateTime) => _sqlExpressionFactory.Convert(instance, typeof(DateTime), _timestampMapping), + + // In PG, date_trunc over timestamptz looks at TimeZone, and returns timestamptz. .NET DateTimeOffset.Date just returns the + // date part (no conversion), and returns an Unspecified DateTime. So we first convert the timestamptz argument to timestamp + // via AT TIME ZONE 'UTC". + // Note that we don't use the overload of date_trunc that accepts a timezone as its 3rd argument (like we do for DateTime.Date), + // since that returns a timestamptz, but DateTimeOffset.Date should return DateTime with Kind=Unspecified + nameof(DateTimeOffset.Date) => + _sqlExpressionFactory.Function( + "date_trunc", + [_sqlExpressionFactory.Constant("day"), _sqlExpressionFactory.AtUtc(instance)], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(DateTime), + _timestampMapping), + + _ => 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). + // To avoid this, if we get a timestamptz, convert it to a timestamp without time zone (at UTC), which doesn't undergo any timezone + // conversions. + private bool TryConvertAwayFromTimestampTz(SqlExpression timestamp, [NotNullWhen(true)] out SqlExpression? result) + { + switch (timestamp) + { + // We're already dealing with a non-timestamptz mapping, no conversion needed. + case { TypeMapping: GaussDBTimestampTypeMapping or GaussDBDateTimeDateTypeMapping or GaussDBTimeTypeMapping }: + case { Type: var type } when type == typeof(DateOnly) || type == typeof(TimeOnly): + result = timestamp; + return true; + + // In these cases we know that the expression represents a timestamptz; it's safe to convert to a timestamp without time zone. + // Note that timestamptz AT TIME ZONE 'UTC' returns the same timestamp but as a timestamp (without time zone). + case { TypeMapping: GaussDBTimestampTzTypeMapping }: + case { Type: var type } when type == typeof(DateTimeOffset): + result = _sqlExpressionFactory.AtUtc(timestamp); + return true; + + // If it's a DateTime who's type mapping isn't known (parameter), we cannot ensure that a timestamp without time zone + // is returned (note that applying AT TIME ZONE 'UTC' on a timestamp without time zone would yield a timestamptz, which would + // again undergo timestamp conversion) + case { Type: var type } when type == typeof(DateTime): + result = null; + return false; + + default: + throw new UnreachableException(); + } + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBDateTimeMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBDateTimeMethodTranslator.cs new file mode 100644 index 0000000000..613e8c451e --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBDateTimeMethodTranslator.cs @@ -0,0 +1,401 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBDateTimeMethodTranslator : IMethodCallTranslator +{ + private static readonly Dictionary MethodInfoDatePartMapping = new() + { + { 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), [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), [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), [])!; + + private static readonly MethodInfo DateTime_ToLocalTime + = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToLocalTime), [])!; + + private static readonly MethodInfo DateTime_SpecifyKind + = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.SpecifyKind), [typeof(DateTime), typeof(DateTimeKind)])!; + + private static readonly MethodInfo DateTime_Distance + = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(DateTime), typeof(DateTime)])!; + + private static readonly MethodInfo DateOnly_FromDateTime + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.FromDateTime), [typeof(DateTime)])!; + + private static readonly MethodInfo DateOnly_ToDateTime + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.ToDateTime), [typeof(TimeOnly)])!; + + private static readonly MethodInfo DateOnly_Distance + = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(DateOnly), typeof(DateOnly)])!; + + private static readonly MethodInfo DateOnly_AddDays + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), [typeof(int)])!; + + private static readonly MethodInfo DateOnly_AddMonths + = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), [typeof(int)])!; + + private static readonly MethodInfo DateOnly_AddYears + = 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), [typeof(DateTime)])!; + + private static readonly MethodInfo TimeOnly_FromTimeSpan + = 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), [typeof(TimeOnly), typeof(TimeOnly)])!; + + private static readonly MethodInfo TimeOnly_Add_TimeSpan + = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), [typeof(TimeSpan)])!; + + private static readonly MethodInfo TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTime + = typeof(TimeZoneInfo).GetRuntimeMethod( + nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [typeof(DateTime), typeof(string)])!; + + private static readonly MethodInfo TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTimeOffset + = typeof(TimeZoneInfo).GetRuntimeMethod( + nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [typeof(DateTimeOffset), typeof(string)])!; + + private static readonly MethodInfo TimeZoneInfo_ConvertTimeToUtc + = typeof(TimeZoneInfo).GetRuntimeMethod(nameof(TimeZoneInfo.ConvertTimeToUtc), [typeof(DateTime)])!; + // ReSharper restore InconsistentNaming + + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _timestampMapping; + private readonly RelationalTypeMapping _timestampTzMapping; + private readonly RelationalTypeMapping _intervalMapping; + private readonly RelationalTypeMapping _textMapping; + + /// + /// 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 GaussDBDateTimeMethodTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _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")!; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => TranslateDateTime(instance, method, arguments) + ?? TranslateDateOnly(instance, method, arguments) + ?? TranslateTimeOnly(instance, method, arguments) + ?? TranslateTimeZoneInfo(method, arguments) + ?? TranslateDatePart(instance, method, arguments); + + private SqlExpression? TranslateDatePart( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + => instance is not null + && MethodInfoDatePartMapping.TryGetValue(method, out var datePart) + && CreateIntervalExpression(arguments[0], datePart) is SqlExpression interval + ? _sqlExpressionFactory.Add(instance, interval, instance.TypeMapping) + : null; + + private SqlExpression? TranslateDateTime( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (instance is null) + { + if (method == DateTime_SpecifyKind) + { + if (arguments[1] is not SqlConstantExpression { Value: DateTimeKind kind }) + { + throw new InvalidOperationException("Translating SpecifyKind is only supported with a constant Kind argument"); + } + + var typeMapping = arguments[0].TypeMapping; + + if (typeMapping is not GaussDBTimestampTypeMapping and not GaussDBTimestampTzTypeMapping) + { + throw new InvalidOperationException("Translating SpecifyKind is only supported on timestamp/timestamptz columns"); + } + + if (kind == DateTimeKind.Utc) + { + return typeMapping is GaussDBTimestampTypeMapping + ? _sqlExpressionFactory.AtUtc(arguments[0]) + : arguments[0]; + } + + if (kind is DateTimeKind.Unspecified or DateTimeKind.Local) + { + return typeMapping is GaussDBTimestampTzTypeMapping + ? _sqlExpressionFactory.AtUtc(arguments[0]) + : arguments[0]; + } + } + + if (method == DateTime_Distance) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.Distance, arguments[1], arguments[2]); + } + } + else + { + if (method == DateTime_ToUniversalTime) + { + return _sqlExpressionFactory.Convert(instance, method.ReturnType, _timestampTzMapping); + } + + if (method == DateTime_ToLocalTime) + { + return _sqlExpressionFactory.Convert(instance, method.ReturnType, _timestampMapping); + } + } + + return null; + } + + private SqlExpression? TranslateDateOnly( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (instance is null) + { + if (method == DateOnly_FromDateTime) + { + // Note: converting timestamptz to date performs a timezone conversion, which is not what .NET DateOnly.FromDateTime does. + // So if our operand is a timestamptz, we first change the type to timestamp with AT TIME ZONE 'UTC' (returns the same value + // but as a timestamptz). + // If our operand is already timestamp, no need to do anything. We throw for anything else to avoid accidentally applying + // AT TIME ZONE to a non-timestamptz, which would do a timezone conversion + var dateTime = arguments[0].TypeMapping switch + { + GaussDBTimestampTypeMapping => arguments[0], + GaussDBTimestampTzTypeMapping => _sqlExpressionFactory.AtUtc(arguments[0]), + _ => throw new NotSupportedException("Can only apply TimeOnly.FromDateTime on a timestamp or timestamptz column") + }; + + return _sqlExpressionFactory.Convert(dateTime, typeof(DateOnly), _typeMappingSource.FindMapping(typeof(DateOnly))); + } + + if (method == DateOnly_Distance) + { + return _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.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 + { + if (method == DateOnly_ToDateTime) + { + return new SqlBinaryExpression( + ExpressionType.Add, + _sqlExpressionFactory.ApplyDefaultTypeMapping(instance), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), + typeof(DateTime), + _timestampMapping); + } + + // In PG, date + int = date (int interpreted as days) + if (method == DateOnly_AddDays) + { + return _sqlExpressionFactory.Add(instance, arguments[0]); + } + + // For months and years, date + interval yields a timestamp (since interval could have a time component), so we need to cast + // the results back to date + if (method == DateOnly_AddMonths + && CreateIntervalExpression(arguments[0], "months") is SqlExpression interval1) + { + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.Add(instance, interval1, instance.TypeMapping), typeof(DateOnly)); + } + + if (method == DateOnly_AddYears + && CreateIntervalExpression(arguments[0], "years") is SqlExpression interval2) + { + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.Add(instance, interval2, instance.TypeMapping), typeof(DateOnly)); + } + } + + return null; + } + + private SqlExpression? TranslateTimeOnly( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments) + { + if (method == TimeOnly_FromDateTime) + { + // Note: converting timestamptz to time performs a timezone conversion, which is not what .NET TimeOnly.FromDateTime does. + // So if our operand is a timestamptz, we first change the type to timestamp with AT TIME ZONE 'UTC' (returns the same value + // but as a timestamptz). + // If our operand is already timestamp, no need to do anything. We throw for anything else to avoid accidentally applying + // AT TIME ZONE to a non-timestamptz, which would do a timezone conversion + var dateTime = arguments[0].TypeMapping switch + { + GaussDBTimestampTypeMapping => arguments[0], + GaussDBTimestampTzTypeMapping => _sqlExpressionFactory.AtUtc(arguments[0]), + _ => throw new NotSupportedException("Can only apply TimeOnly.FromDateTime on a timestamp or timestamptz column") + }; + + return _sqlExpressionFactory.Convert( + dateTime, + typeof(TimeOnly), + _typeMappingSource.FindMapping(typeof(TimeOnly))); + } + + if (method == TimeOnly_FromTimeSpan) + { + return _sqlExpressionFactory.Convert(arguments[0], typeof(TimeOnly), _typeMappingSource.FindMapping(typeof(TimeOnly))); + } + + if (instance is not null) + { + if (method == TimeOnly_ToTimeSpan) + { + return _sqlExpressionFactory.Convert(instance, typeof(TimeSpan), _typeMappingSource.FindMapping(typeof(TimeSpan))); + } + + if (method == TimeOnly_IsBetween) + { + return _sqlExpressionFactory.And( + _sqlExpressionFactory.GreaterThanOrEqual(instance, arguments[0]), + _sqlExpressionFactory.LessThan(instance, arguments[1])); + } + + if (method == TimeOnly_Add_TimeSpan) + { + return _sqlExpressionFactory.Add(instance, arguments[0]); + } + } + + return null; + } + + private SqlExpression? TranslateTimeZoneInfo( + MethodInfo method, + IReadOnlyList arguments) + { + if (method == TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTime) + { + var typeMapping = arguments[0].TypeMapping; + if (typeMapping is null + || (typeMapping.StoreType != "timestamp with time zone" && typeMapping.StoreType != "timestamptz")) + { + throw new InvalidOperationException( + "TimeZoneInfo.ConvertTimeBySystemTimeZoneId is only supported on columns with type 'timestamp with time zone'"); + } + + return _sqlExpressionFactory.AtTimeZone(arguments[0], arguments[1], typeof(DateTime), _timestampMapping); + } + + if (method == TimeZoneInfo_ConvertTimeToUtc) + { + var typeMapping = arguments[0].TypeMapping; + if (typeMapping is null + || (typeMapping.StoreType != "timestamp without time zone" && typeMapping.StoreType != "timestamp")) + { + throw new InvalidOperationException( + "TimeZoneInfo.ConvertTimeToUtc) is only supported on columns with type 'timestamp without time zone'"); + } + + return _sqlExpressionFactory.Convert(arguments[0], arguments[0].Type, _timestampTzMapping); + } + + return null; + } + + private SqlExpression? CreateIntervalExpression(SqlExpression intervalNum, string datePart) + { + // Note: ideally we'd simply generate a GaussDB interval expression, but the .NET mapping of that is TimeSpan, + // which does not work for months, years, etc. So we generate special fragments instead. + if (intervalNum is SqlConstantExpression constantExpression) + { + // We generate constant intervals as INTERVAL '1 days' + if (constantExpression.Type == typeof(double) + && ((double)constantExpression.Value! >= int.MaxValue || (double)constantExpression.Value <= int.MinValue)) + { + return null; + } + + return _sqlExpressionFactory.Fragment(FormattableString.Invariant($"INTERVAL '{constantExpression.Value} {datePart}'")); + } + + // For non-constants, we can't parameterize INTERVAL '1 days'. Instead, we use CAST($1 || ' days' AS interval). + // Note that a make_interval() function also exists, but accepts only int (for all fields except for + // seconds), so we don't use it. + // Note: we instantiate SqlBinaryExpression manually rather than via sqlExpressionFactory because + // of the non-standard Add expression (concatenate int with text) + return _sqlExpressionFactory.Convert( + new SqlBinaryExpression( + ExpressionType.Add, + _sqlExpressionFactory.Convert(intervalNum, typeof(string), _textMapping), + _sqlExpressionFactory.Constant(' ' + datePart, _textMapping), + typeof(string), + _textMapping), + typeof(TimeSpan), + _intervalMapping); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBFullTextSearchMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBFullTextSearchMethodTranslator.cs new file mode 100644 index 0000000000..f3e191a746 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBFullTextSearchMethodTranslator.cs @@ -0,0 +1,350 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Provides translations for GaussDB full-text search methods. +/// +public class GaussDBFullTextSearchMethodTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo TsQueryParse = + typeof(GaussDBTsQuery).GetMethod(nameof(GaussDBTsQuery.Parse), BindingFlags.Public | BindingFlags.Static)!; + + private static readonly MethodInfo TsVectorParse = + typeof(GaussDBTsVector).GetMethod(nameof(GaussDBTsVector.Parse), BindingFlags.Public | BindingFlags.Static)!; + + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IModel _model; + private readonly RelationalTypeMapping _tsQueryMapping; + private readonly RelationalTypeMapping _tsVectorMapping; + private readonly RelationalTypeMapping _regconfigMapping; + private readonly RelationalTypeMapping _regdictionaryMapping; + + /// + /// 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 GaussDBFullTextSearchMethodTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _model = model; + _tsQueryMapping = typeMappingSource.FindMapping("tsquery")!; + _tsVectorMapping = typeMappingSource.FindMapping("tsvector")!; + _regconfigMapping = typeMappingSource.FindMapping("regconfig")!; + _regdictionaryMapping = typeMappingSource.FindMapping("regdictionary")!; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == TsQueryParse || method == TsVectorParse) + { + return _sqlExpressionFactory.Convert(arguments[0], method.ReturnType); + } + + if (method.DeclaringType == typeof(GaussDBFullTextSearchDbFunctionsExtensions)) + { + return method.Name switch + { + // Methods accepting a configuration (regconfig) + nameof(GaussDBFullTextSearchDbFunctionsExtensions.ToTsVector) when arguments.Count == 3 + => ConfigAccepting("to_tsvector"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.PlainToTsQuery) when arguments.Count == 3 + => ConfigAccepting("plainto_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.PhraseToTsQuery) when arguments.Count == 3 + => ConfigAccepting("phraseto_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.ToTsQuery) when arguments.Count == 3 + => ConfigAccepting("to_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.WebSearchToTsQuery) when arguments.Count == 3 + => ConfigAccepting("websearch_to_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.Unaccent) when arguments.Count == 3 + => DictionaryAccepting("unaccent"), + + // Methods not accepting a configuration + nameof(GaussDBFullTextSearchDbFunctionsExtensions.ArrayToTsVector) + => NonConfigAccepting("array_to_tsvector"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.ToTsVector) + => NonConfigAccepting("to_tsvector"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.PlainToTsQuery) + => NonConfigAccepting("plainto_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.PhraseToTsQuery) + => NonConfigAccepting("phraseto_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.ToTsQuery) + => NonConfigAccepting("to_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.WebSearchToTsQuery) + => NonConfigAccepting("websearch_to_tsquery"), + nameof(GaussDBFullTextSearchDbFunctionsExtensions.Unaccent) + => NonConfigAccepting("unaccent"), + + _ => null + }; + } + + if (method.DeclaringType == typeof(GaussDBFullTextSearchLinqExtensions)) + { + if (method.Name is + nameof(GaussDBFullTextSearchLinqExtensions.Rank) or nameof(GaussDBFullTextSearchLinqExtensions.RankCoverDensity)) + { + var rankFunctionName = method.Name == nameof(GaussDBFullTextSearchLinqExtensions.Rank) ? "ts_rank" : "ts_rank_cd"; + + return arguments.Count switch + { + 2 => _sqlExpressionFactory.Function( + rankFunctionName, + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(float), + _typeMappingSource.FindMapping(typeof(float), _model)), + + 3 => _sqlExpressionFactory.Function( + rankFunctionName, + [ + 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), + _typeMappingSource.FindMapping(typeof(float), _model)), + + 4 => _sqlExpressionFactory.Function( + rankFunctionName, + [arguments[1], arguments[0], arguments[2], arguments[3]], + nullable: true, + argumentsPropagateNullability: TrueArrays[4], + method.ReturnType, + _typeMappingSource.FindMapping(typeof(float), _model)), + + _ => throw new ArgumentException($"Invalid method overload for {rankFunctionName}") + }; + } + + if (method.Name == nameof(GaussDBFullTextSearchLinqExtensions.SetWeight)) + { + var newArgs = new List(arguments); + if (newArgs[1].Type == typeof(GaussDBTsVector.Lexeme.Weight)) + { + newArgs[1] = newArgs[1] is SqlConstantExpression weightExpression + ? _sqlExpressionFactory.Constant(weightExpression.Value!.ToString()![0]) + : throw new ArgumentException("Enum 'weight' argument for 'SetWeight' must be a constant expression."); + } + + return _sqlExpressionFactory.Function( + "setweight", + newArgs, + nullable: true, + argumentsPropagateNullability: TrueArrays[newArgs.Count], + method.ReturnType); + } + + return method.Name switch + { + // Operators + + nameof(GaussDBFullTextSearchLinqExtensions.And) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.TextSearchAnd, arguments[0], arguments[1]), + nameof(GaussDBFullTextSearchLinqExtensions.Or) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.TextSearchOr, arguments[0], arguments[1]), + + nameof(GaussDBFullTextSearchLinqExtensions.ToNegative) + => new SqlUnaryExpression( + ExpressionType.Not, arguments[0], arguments[0].Type, + arguments[0].TypeMapping), + + nameof(GaussDBFullTextSearchLinqExtensions.Contains) + => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), + nameof(GaussDBFullTextSearchLinqExtensions.IsContainedIn) + => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), + + nameof(GaussDBFullTextSearchLinqExtensions.Concat) + => _sqlExpressionFactory.Add(arguments[0], arguments[1], _tsVectorMapping), + + nameof(GaussDBFullTextSearchLinqExtensions.Matches) + => _sqlExpressionFactory.MakePostgresBinary( + GaussDBExpressionType.TextSearchMatch, + arguments[0], + arguments[1].Type == typeof(string) + ? _sqlExpressionFactory.Function( + "plainto_tsquery", + [arguments[1]], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(GaussDBTsQuery), + _tsQueryMapping) + : arguments[1]), + + // Functions + + nameof(GaussDBFullTextSearchLinqExtensions.GetNodeCount) + => _sqlExpressionFactory.Function( + "numnode", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(int), + _typeMappingSource.FindMapping(method.ReturnType, _model)), + + nameof(GaussDBFullTextSearchLinqExtensions.GetQueryTree) + => _sqlExpressionFactory.Function( + "querytree", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(string), + _typeMappingSource.FindMapping(method.ReturnType, _model)), + + nameof(GaussDBFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 2 + => _sqlExpressionFactory.Function( + "ts_headline", + [arguments[1], arguments[0]], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + + nameof(GaussDBFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 3 => + _sqlExpressionFactory.Function( + "ts_headline", + [arguments[1], arguments[0], arguments[2]], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + method.ReturnType), + + nameof(GaussDBFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 4 => + _sqlExpressionFactory.Function( + "ts_headline", + [ + // 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. + arguments[1] is SqlConstantExpression constant + ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) + : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regconfigMapping), + arguments[2], + arguments[0], + arguments[3] + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[4], + method.ReturnType), + + nameof(GaussDBFullTextSearchLinqExtensions.Rewrite) when arguments.Count == 2 + => _sqlExpressionFactory.Function( + "ts_rewrite", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(GaussDBTsQuery), + _tsQueryMapping), + + nameof(GaussDBFullTextSearchLinqExtensions.Rewrite) when arguments.Count == 3 + => _sqlExpressionFactory.Function( + "ts_rewrite", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + typeof(GaussDBTsQuery), + _tsQueryMapping), + + nameof(GaussDBFullTextSearchLinqExtensions.ToPhrase) + => _sqlExpressionFactory.Function( + "tsquery_phrase", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[arguments.Count], + typeof(GaussDBTsQuery), + _tsQueryMapping), + + nameof(GaussDBFullTextSearchLinqExtensions.Delete) + => _sqlExpressionFactory.Function( + "ts_delete", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + _tsVectorMapping), + + // TODO: Here we need to cast the char[] array we got into a "char"[] internal array... + nameof(GaussDBFullTextSearchLinqExtensions.Filter) + => throw new NotImplementedException(), + //=> _sqlExpressionFactory.Function("ts_filter", arguments, typeof(GaussDBTsVector), _tsVectorMapping), + + nameof(GaussDBFullTextSearchLinqExtensions.GetLength) + => _sqlExpressionFactory.Function( + "length", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + method.ReturnType, + _typeMappingSource.FindMapping(typeof(int), _model)), + + nameof(GaussDBFullTextSearchLinqExtensions.ToStripped) + => _sqlExpressionFactory.Function( + "strip", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[arguments.Count], + method.ReturnType, + _tsVectorMapping), + + _ => null + }; + } + + return null; + + SqlExpression ConfigAccepting(string functionName) + => _sqlExpressionFactory.Function( + 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. + arguments[1] is SqlConstantExpression constant + ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) + : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regconfigMapping), + arguments[2] + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + _typeMappingSource.FindMapping(method.ReturnType, _model)); + + SqlExpression DictionaryAccepting(string functionName) + => _sqlExpressionFactory.Function( + 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. + arguments[1] is SqlConstantExpression constant + ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) + : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regdictionaryMapping), + arguments[2] + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + _typeMappingSource.FindMapping(method.ReturnType, _model)); + + SqlExpression NonConfigAccepting(string functionName) + => _sqlExpressionFactory.Function( + functionName, + [arguments[1]], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + method.ReturnType, + _typeMappingSource.FindMapping(method.ReturnType, _model)); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBFuzzyStringMatchMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBFuzzyStringMatchMethodTranslator.cs new file mode 100644 index 0000000000..2435322003 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBFuzzyStringMatchMethodTranslator.cs @@ -0,0 +1,74 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBFuzzyStringMatchMethodTranslator : IMethodCallTranslator +{ + private static readonly Dictionary Functions = new() + { + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchSoundex), typeof(DbFunctions), typeof(string))] + = "soundex", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDifference), typeof(DbFunctions), typeof(string), typeof(string))] + = "difference", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein), typeof(DbFunctions), typeof(string), typeof(string))] + = "levenshtein", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein), typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(int), typeof(int))] + = "levenshtein", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshteinLessEqual), typeof(DbFunctions), typeof(string), typeof(string), typeof(int))] + = "levenshtein_less_equal", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshteinLessEqual), typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(int), typeof(int), typeof(int))] + = "levenshtein_less_equal", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchMetaphone), typeof(DbFunctions), typeof(string), typeof(int))] + = "metaphone", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDoubleMetaphone), typeof(DbFunctions), typeof(string))] + = "dmetaphone", + [GetRuntimeMethod(nameof(GaussDBFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDoubleMetaphoneAlt), typeof(DbFunctions), typeof(string))] + = "dmetaphone_alt" + }; + + private static MethodInfo GetRuntimeMethod(string name, params Type[] parameters) + => typeof(GaussDBFuzzyStringMatchDbFunctionsExtensions).GetRuntimeMethod(name, parameters)!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + + private static readonly bool[][] TrueArrays = + [ + [], + [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 + /// 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 GaussDBFuzzyStringMatchMethodTranslator(GaussDBExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => Functions.TryGetValue(method, out var function) + ? _sqlExpressionFactory.Function( + function, + arguments.Skip(1), + nullable: true, + argumentsPropagateNullability: TrueArrays[arguments.Count - 1], + method.ReturnType) + : null; +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBGuidTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBGuidTranslator.cs new file mode 100644 index 0000000000..1d85872283 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBGuidTranslator.cs @@ -0,0 +1,57 @@ +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Provides translation services for GaussDB UUID functions. +/// +/// +/// See: https://www.postgresql.org/docs/current/datatype-uuid.html +/// +public class GaussDBGuidTranslator(ISqlExpressionFactory sqlExpressionFactory, Version? postgresVersion) : IMethodCallTranslator +{ + private readonly string _uuidGenerationFunction = postgresVersion.AtLeast(13) ? "gen_random_uuid" : "uuid_generate_v4"; + + /// + /// 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? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(Guid)) + { + return method.Name switch + { + nameof(Guid.NewGuid) + => sqlExpressionFactory.Function( + _uuidGenerationFunction, + [], + nullable: false, + argumentsPropagateNullability: FalseArrays[0], + method.ReturnType), + + // Note: uuidv7() was introduce in GaussDB 18. + // In GaussDBEvaluatableExpressionFilter we only prevent local evaluation when targeting PG18 or later; + // that means that for lower version, the call gets evaluated locally and the result sent as a parameter + // (and we never see the method call here). + nameof(Guid.CreateVersion7) + => sqlExpressionFactory.Function( + "uuidv7", + [], + nullable: false, + argumentsPropagateNullability: FalseArrays[0], + method.ReturnType), + + _ => null + }; + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonDbFunctionsTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonDbFunctionsTranslator.cs new file mode 100644 index 0000000000..a565dbd8f0 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonDbFunctionsTranslator.cs @@ -0,0 +1,128 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBJsonDbFunctionsTranslator : IMethodCallTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _stringTypeMapping; + private readonly RelationalTypeMapping _jsonbTypeMapping; + + /// + /// 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 GaussDBJsonDbFunctionsTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), model)!; + _jsonbTypeMapping = typeMappingSource.FindMapping("jsonb")!; + } + + /// + /// 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? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType != typeof(GaussDBJsonDbFunctionsExtensions)) + { + return null; + } + + var args = arguments + // Skip useless DbFunctions instance + .Skip(1) + // JSON extensions accept object parameters for JSON, since they must be able to handle POCOs, strings or DOM types. + // This means they come wrapped in a convert node, which we need to remove. + // Convert nodes may also come from wrapping JsonTraversalExpressions generated through POCO traversal. + .Select(RemoveConvert) + // If a function is invoked over a JSON traversal expression, that expression may come with + // returnText: true (i.e. operator ->> and not ->). Since the functions below require a json object and + // not text, we transform it. + .Select(a => a is GaussDBJsonTraversalExpression traversal ? WithReturnsText(traversal, false) : a) + .ToArray(); + + if (!args.Any(a => a.TypeMapping is GaussDBJsonTypeMapping || a is GaussDBJsonTraversalExpression)) + { + throw new InvalidOperationException("The EF JSON methods require a JSON parameter and none was found."); + } + + if (method.Name == nameof(GaussDBJsonDbFunctionsExtensions.JsonTypeof)) + { + return _sqlExpressionFactory.Function( + ((GaussDBJsonTypeMapping)args[0].TypeMapping!).IsJsonb ? "jsonb_typeof" : "json_typeof", + [args[0]], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(string)); + } + + // The following are jsonb-only, not support on json + if (args.Any(a => a.TypeMapping is GaussDBJsonTypeMapping { IsJsonb: false })) + { + throw new InvalidOperationException("JSON methods on EF.Functions only support the jsonb type, not json."); + } + + return method.Name switch + { + nameof(GaussDBJsonDbFunctionsExtensions.JsonContains) + => _sqlExpressionFactory.Contains(Jsonb(args[0]), Jsonb(args[1])), + nameof(GaussDBJsonDbFunctionsExtensions.JsonContained) + => _sqlExpressionFactory.ContainedBy(Jsonb(args[0]), Jsonb(args[1])), + nameof(GaussDBJsonDbFunctionsExtensions.JsonExists) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.JsonExists, Jsonb(args[0]), args[1]), + nameof(GaussDBJsonDbFunctionsExtensions.JsonExistAny) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.JsonExistsAny, Jsonb(args[0]), args[1]), + nameof(GaussDBJsonDbFunctionsExtensions.JsonExistAll) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.JsonExistsAll, Jsonb(args[0]), args[1]), + + _ => null + }; + + SqlExpression Jsonb(SqlExpression e) + => e.TypeMapping?.StoreType == "jsonb" + ? e + : e is SqlConstantExpression or SqlParameterExpression + ? _sqlExpressionFactory.ApplyTypeMapping(e, _jsonbTypeMapping) + : _sqlExpressionFactory.Convert(e, typeof(string), _jsonbTypeMapping); + + static SqlExpression RemoveConvert(SqlExpression e) + { + while (e is SqlUnaryExpression { OperatorType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) + { + e = unary.Operand; + } + + return e; + } + + GaussDBJsonTraversalExpression WithReturnsText(GaussDBJsonTraversalExpression traversal, bool returnsText) + => traversal.ReturnsText == returnsText + ? traversal + : returnsText + ? new GaussDBJsonTraversalExpression(traversal.Expression, traversal.Path, true, typeof(string), _stringTypeMapping) + : new GaussDBJsonTraversalExpression( + traversal.Expression, traversal.Path, false, traversal.Type, traversal.Expression.TypeMapping); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonDomTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonDomTranslator.cs new file mode 100644 index 0000000000..5e67d071a0 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonDomTranslator.cs @@ -0,0 +1,157 @@ +using System.Text.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBJsonDomTranslator : IMemberTranslator, IMethodCallTranslator +{ + private static readonly MemberInfo RootElement = typeof(JsonDocument).GetProperty(nameof(JsonDocument.RootElement))!; + + private static readonly MethodInfo GetProperty = typeof(JsonElement).GetRuntimeMethod( + nameof(JsonElement.GetProperty), [typeof(string)])!; + + private static readonly MethodInfo GetArrayLength = typeof(JsonElement).GetRuntimeMethod( + nameof(JsonElement.GetArrayLength), Type.EmptyTypes)!; + + private static readonly MethodInfo ArrayIndexer = typeof(JsonElement).GetProperties() + .Single(p => p.GetIndexParameters().Length == 1 && p.GetIndexParameters()[0].ParameterType == typeof(int)) + .GetMethod!; + + private static readonly string[] GetMethods = + [ + nameof(JsonElement.GetBoolean), + nameof(JsonElement.GetDateTime), + nameof(JsonElement.GetDateTimeOffset), + nameof(JsonElement.GetDecimal), + nameof(JsonElement.GetDouble), + nameof(JsonElement.GetGuid), + nameof(JsonElement.GetInt16), + nameof(JsonElement.GetInt32), + nameof(JsonElement.GetInt64), + nameof(JsonElement.GetSingle), + nameof(JsonElement.GetString) + ]; + + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _stringTypeMapping; + private readonly 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 + /// 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 GaussDBJsonDomTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _model = model; + _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), 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 SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (member.DeclaringType != typeof(JsonDocument)) + { + return null; + } + + if (member == RootElement && instance is ColumnExpression { TypeMapping: GaussDBJsonTypeMapping } column) + { + // Simply get rid of the RootElement member access + return column; + } + + return 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 virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType != typeof(JsonElement) || instance?.TypeMapping is not GaussDBJsonTypeMapping mapping) + { + return null; + } + + // The root of the JSON expression is a ColumnExpression. We wrap that with an empty traversal + // expression (col #>> '{}'); subsequent traversals will gradually append the path into that. + // Note that it's possible to call methods such as GetString() directly on the root, and the + // empty traversal is necessary to properly convert it to a text. + instance = instance is ColumnExpression columnExpression + ? _sqlExpressionFactory.JsonTraversal( + columnExpression, returnsText: false, typeof(string), mapping) + : instance; + + if (method == GetProperty || method == ArrayIndexer) + { + return instance is GaussDBJsonTraversalExpression prevPathTraversal + ? prevPathTraversal.Append(_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0])) + : null; + } + + if (GetMethods.Contains(method.Name) && arguments.Count == 0 && instance is GaussDBJsonTraversalExpression traversal) + { + var traversalToText = new GaussDBJsonTraversalExpression( + traversal.Expression, + traversal.Path, + returnsText: true, + typeof(string), + _stringTypeMapping); + + // The GaussDB traversal operator always returns text - for these scalar-returning methods, apply a conversion from string. + return method.Name == nameof(JsonElement.GetString) + ? traversalToText + : _sqlExpressionFactory.Convert( + traversalToText, method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, _model)); + } + + if (method == GetArrayLength) + { + return _sqlExpressionFactory.Function( + mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(int)); + } + + if (method.Name.StartsWith("TryGet", StringComparison.Ordinal) && arguments.Count == 0) + { + throw new InvalidOperationException($"The TryGet* methods on {nameof(JsonElement)} aren't translated yet, use Get* instead.'"); + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonPocoTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonPocoTranslator.cs new file mode 100644 index 0000000000..18f178d684 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBJsonPocoTranslator.cs @@ -0,0 +1,182 @@ +using System.Text.Json.Serialization; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBJsonPocoTranslator : IMemberTranslator, IMethodCallTranslator +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _stringTypeMapping; + private readonly IModel _model; + + private static readonly MethodInfo Enumerable_AnyWithoutPredicate = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1); + + /// + /// 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 GaussDBJsonPocoTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _model = model; + _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), 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 SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Predicate-less Any - translate to a simple length check. + if (method.IsClosedFormOf(Enumerable_AnyWithoutPredicate) + && TranslateArrayLength(arguments[0]) is SqlExpression arrayLengthTranslation) + { + return _sqlExpressionFactory.GreaterThan(arrayLengthTranslation, _sqlExpressionFactory.Constant(0)); + } + + return 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 virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (instance?.TypeMapping is not GaussDBJsonTypeMapping && instance is not GaussDBJsonTraversalExpression) + { + return null; + } + + if (member is { Name: "Count", DeclaringType.IsGenericType: true } + && member.DeclaringType.GetGenericTypeDefinition() == typeof(List<>)) + { + return TranslateArrayLength(instance); + } + + return TranslateMemberAccess( + instance, + _sqlExpressionFactory.Constant(member.GetCustomAttribute()?.Name ?? member.Name), + returnType); + } + + /// + /// 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? TranslateMemberAccess(SqlExpression instance, SqlExpression member, Type returnType) + { + 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. + ColumnExpression { TypeMapping: GaussDBJsonTypeMapping } columnExpression + => ConvertFromText( + _sqlExpressionFactory.JsonTraversal( + columnExpression, + [member], + returnsText: true, + typeof(string), + _stringTypeMapping), + returnType), + + GaussDBJsonTraversalExpression prevPathTraversal + => ConvertFromText( + prevPathTraversal.Append(_sqlExpressionFactory.ApplyDefaultTypeMapping(member)), + returnType), + + _ => null + }; + + // The GaussDB 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 + GaussDBArrayTypeMapping => 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 + /// 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? TranslateArrayLength(SqlExpression expression) + { + switch (expression) + { + case ColumnExpression { TypeMapping: GaussDBJsonTypeMapping mapping }: + return _sqlExpressionFactory.Function( + mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", + [expression], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(int)); + + case GaussDBJsonTraversalExpression traversal: + // The traversal expression has ReturnsText=true (e.g. ->> not ->), so we recreate it to return + // the JSON object instead. + var lastPathComponent = traversal.Path.Last(); + var newTraversal = new GaussDBJsonTraversalExpression( + traversal.Expression, traversal.Path, + returnsText: false, + lastPathComponent.Type, + _typeMappingSource.FindMapping(lastPathComponent.Type, _model)); + + var jsonMapping = (GaussDBJsonTypeMapping)traversal.Expression.TypeMapping!; + return _sqlExpressionFactory.Function( + jsonMapping.IsJsonb ? "jsonb_array_length" : "json_array_length", + [newTraversal], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(int)); + + default: + return null; + } + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBLTreeTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBLTreeTranslator.cs new file mode 100644 index 0000000000..6208564254 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBLTreeTranslator.cs @@ -0,0 +1,161 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBLTreeTranslator : IMethodCallTranslator, IMemberTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _boolTypeMapping; + private readonly RelationalTypeMapping _ltreeTypeMapping; + private readonly RelationalTypeMapping _lqueryTypeMapping; + private readonly RelationalTypeMapping _ltxtqueryTypeMapping; + + /// + /// 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 GaussDBLTreeTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _boolTypeMapping = typeMappingSource.FindMapping(typeof(bool), model)!; + _ltreeTypeMapping = typeMappingSource.FindMapping(typeof(LTree), model)!; + _lqueryTypeMapping = typeMappingSource.FindMapping("lquery")!; + _ltxtqueryTypeMapping = typeMappingSource.FindMapping("ltxtquery")!; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(LTree)) + { + return method.Name switch + { + nameof(LTree.IsAncestorOf) + => new GaussDBBinaryExpression( + GaussDBExpressionType.Contains, + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), + typeof(bool), + _boolTypeMapping), + + nameof(LTree.IsDescendantOf) + => new GaussDBBinaryExpression( + GaussDBExpressionType.ContainedBy, + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), + typeof(bool), + _boolTypeMapping), + + nameof(LTree.MatchesLQuery) + => new GaussDBBinaryExpression( + GaussDBExpressionType.LTreeMatches, + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _lqueryTypeMapping), + typeof(bool), + _boolTypeMapping), + + nameof(LTree.MatchesLTxtQuery) + => new GaussDBBinaryExpression( + GaussDBExpressionType.LTreeMatches, + ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), + ApplyTypeMappingOrConvert(arguments[0], _ltxtqueryTypeMapping), + typeof(bool), + _boolTypeMapping), + + nameof(LTree.Subtree) + => _sqlExpressionFactory.Function( + "subltree", + [instance!, arguments[0], arguments[1]], + nullable: true, + TrueArrays[3], + typeof(LTree), + _ltreeTypeMapping), + + nameof(LTree.Subpath) + => _sqlExpressionFactory.Function( + "subpath", + arguments.Count == 2 + ? [instance!, arguments[0], arguments[1]] + : new[] { instance!, arguments[0] }, + nullable: true, + arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], + typeof(LTree), + _ltreeTypeMapping), + + nameof(LTree.Index) + => _sqlExpressionFactory.Function( + "index", + arguments.Count == 2 + ? [instance!, arguments[0], arguments[1]] + : new[] { instance!, arguments[0] }, + nullable: true, + arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], + typeof(int)), + + nameof(LTree.LongestCommonAncestor) + => _sqlExpressionFactory.Function( + "lca", + [arguments[0]], + nullable: true, + TrueArrays[1], + typeof(LTree), + _ltreeTypeMapping), + + _ => null + }; + } + + return 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 virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + => member.DeclaringType == typeof(LTree) && member.Name == nameof(LTree.NLevel) + ? _sqlExpressionFactory.Function( + "nlevel", + [instance!], + nullable: true, + TrueArrays[1], + typeof(int)) + : null; + + // Applying e.g. the LQuery type mapping on a function operator is a bit tricky. + // If it's a constant, we can just apply the mapping: the constant will get rendered as an untyped string literal, and PG will + // coerce it as the function parameter. + // If it's a parameter, we can also just apply the mapping (which causes GaussDBDbType to be set to LQuery). + // For anything else, we may need an explicit cast to LQuery, e.g. a plain text column or a concatenation between strings; + // apply the default type mapping and then apply an additional Convert node if the resulting mapping isn't what we need. + private SqlExpression ApplyTypeMappingOrConvert(SqlExpression sqlExpression, RelationalTypeMapping typeMapping) + => sqlExpression is SqlConstantExpression or SqlParameterExpression + ? _sqlExpressionFactory.ApplyTypeMapping(sqlExpression, typeMapping) + : _sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpression) is var expressionWithDefaultTypeMapping + && expressionWithDefaultTypeMapping.TypeMapping!.StoreType == typeMapping.StoreType + ? expressionWithDefaultTypeMapping + : _sqlExpressionFactory.Convert(expressionWithDefaultTypeMapping, typeMapping.ClrType, typeMapping); +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBLikeTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBLikeTranslator.cs new file mode 100644 index 0000000000..1702662a32 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBLikeTranslator.cs @@ -0,0 +1,93 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Translates methods into GaussDB LIKE expressions. +/// +public class GaussDBLikeTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo Like = + typeof(DbFunctionsExtensions).GetRuntimeMethod( + nameof(DbFunctionsExtensions.Like), + [typeof(DbFunctions), typeof(string), typeof(string)])!; + + private static readonly MethodInfo LikeWithEscape = + typeof(DbFunctionsExtensions).GetRuntimeMethod( + nameof(DbFunctionsExtensions.Like), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; + + // ReSharper disable once InconsistentNaming + private static readonly MethodInfo ILike = + typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.ILike), + [typeof(DbFunctions), typeof(string), typeof(string)])!; + + // ReSharper disable once InconsistentNaming + private static readonly MethodInfo ILikeWithEscape = + typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.ILike), + [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The SQL expression factory to use when generating expressions.. + public GaussDBLikeTranslator(GaussDBExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == LikeWithEscape) + { + return _sqlExpressionFactory.Like(arguments[1], arguments[2], arguments[3]); + } + + if (method == ILikeWithEscape) + { + return _sqlExpressionFactory.ILike(arguments[1], arguments[2], arguments[3]); + } + + bool sensitive; + if (method == Like) + { + sensitive = true; + } + else if (method == ILike) + { + sensitive = false; + } + else + { + return null; + } + + // GaussDB has backslash as the default LIKE escape character, but EF Core expects + // no escape character unless explicitly requested (https://github.com/aspnet/EntityFramework/issues/8696). + + // If we have a constant expression, we check that there are no backslashes in order to render with + // an ESCAPE clause (better SQL). If we have a constant expression with backslashes or a non-constant + // expression, we render an ESCAPE clause to disable backslash escaping. + + var (match, pattern) = (arguments[1], arguments[2]); + + if (pattern is SqlConstantExpression { Value: string patternValue } + && !patternValue.Contains('\\')) + { + return sensitive + ? _sqlExpressionFactory.Like(match, pattern) + : _sqlExpressionFactory.ILike(match, pattern); + } + + return sensitive + ? _sqlExpressionFactory.Like(match, pattern, _sqlExpressionFactory.Constant(string.Empty)) + : _sqlExpressionFactory.ILike(match, pattern, _sqlExpressionFactory.Constant(string.Empty)); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMathTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMathTranslator.cs new file mode 100644 index 0000000000..2a52d4c9ae --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMathTranslator.cs @@ -0,0 +1,290 @@ +using System.Numerics; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Provides translation services for static methods.. +/// +/// +/// 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 GaussDBMathTranslator : IMethodCallTranslator +{ + private static readonly Dictionary SupportedMethodTranslations = new() + { + { 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: GaussDB has log(x,y) but only for decimal, whereas .NET has it only for double/float + + { 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), [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), [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 = + [ + 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 = + [ + 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 = + [ + 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), [typeof(decimal), typeof(int)])!; + + private static readonly MethodInfo DoubleIsNanMethodInfo + = typeof(double).GetRuntimeMethod(nameof(double.IsNaN), [typeof(double)])!; + + private static readonly MethodInfo DoubleIsPositiveInfinityMethodInfo + = typeof(double).GetRuntimeMethod(nameof(double.IsPositiveInfinity), [typeof(double)])!; + + private static readonly MethodInfo DoubleIsNegativeInfinityMethodInfo + = typeof(double).GetRuntimeMethod(nameof(double.IsNegativeInfinity), [typeof(double)])!; + + private static readonly MethodInfo FloatIsNanMethodInfo + = typeof(float).GetRuntimeMethod(nameof(float.IsNaN), [typeof(float)])!; + + private static readonly MethodInfo FloatIsPositiveInfinityMethodInfo + = typeof(float).GetRuntimeMethod(nameof(float.IsPositiveInfinity), [typeof(float)])!; + + private static readonly MethodInfo FloatIsNegativeInfinityMethodInfo + = typeof(float).GetRuntimeMethod(nameof(float.IsNegativeInfinity), [typeof(float)])!; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _intTypeMapping; + private readonly RelationalTypeMapping _decimalTypeMapping; + + /// + /// 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 GaussDBMathTranslator( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _intTypeMapping = typeMappingSource.FindMapping(typeof(int), model)!; + _decimalTypeMapping = typeMappingSource.FindMapping(typeof(decimal), model)!; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (SupportedMethodTranslations.TryGetValue(method, out var sqlFunctionName)) + { + var typeMapping = arguments.Count == 1 + ? ExpressionExtensions.InferTypeMapping(arguments[0]) + : ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]); + + var newArguments = new SqlExpression[arguments.Count]; + newArguments[0] = _sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping); + + if (arguments.Count == 2) + { + newArguments[1] = _sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping); + } + + // Note: GREATER/LEAST only return NULL if *all* arguments are null, but we currently can't + // convey this. + return _sqlExpressionFactory.Function( + sqlFunctionName, + newArguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[newArguments.Length], + method.ReturnType, + typeMapping); + } + + if (TruncateMethodInfos.Contains(method)) + { + var argument = arguments[0]; + + // 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 = _sqlExpressionFactory.Function( + "trunc", + [argument], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + argument.Type == typeof(float) ? typeof(double) : argument.Type); + + if (argument.Type == typeof(float)) + { + result = _sqlExpressionFactory.Convert(result, typeof(float)); + } + + return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping); + } + + if (RoundMethodInfos.Contains(method)) + { + var argument = arguments[0]; + + // 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 = _sqlExpressionFactory.Function( + "round", + [argument], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + argument.Type == typeof(float) ? typeof(double) : argument.Type); + + if (argument.Type == typeof(float)) + { + result = _sqlExpressionFactory.Convert(result, typeof(float)); + } + + return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping); + } + + // GaussDB sign() returns 1, 0, -1, but in the same type as the argument, so we need to convert + // the return type to int. + if (SignMethodInfos.Contains(method)) + { + return + _sqlExpressionFactory.Convert( + _sqlExpressionFactory.Function( + "sign", + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + method.ReturnType), + typeof(int), + _intTypeMapping); + } + + if (method == RoundDecimalTwoParams) + { + return _sqlExpressionFactory.Function( + "round", + [ + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]) + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + _decimalTypeMapping); + } + + // GaussDB treats NaN values as equal, against IEEE754 + if (method == DoubleIsNanMethodInfo) + { + return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(double.NaN)); + } + + if (method == FloatIsNanMethodInfo) + { + return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(float.NaN)); + } + + if (method == DoubleIsPositiveInfinityMethodInfo) + { + return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(double.PositiveInfinity)); + } + + if (method == FloatIsPositiveInfinityMethodInfo) + { + return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(float.PositiveInfinity)); + } + + if (method == DoubleIsNegativeInfinityMethodInfo) + { + return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(double.NegativeInfinity)); + } + + if (method == FloatIsNegativeInfinityMethodInfo) + { + return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(float.NegativeInfinity)); + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMemberTranslatorProvider.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMemberTranslatorProvider.cs new file mode 100644 index 0000000000..f62adad2b0 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMemberTranslatorProvider.cs @@ -0,0 +1,49 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// A composite member translator that dispatches to multiple specialized member translators specific to GaussDB. +/// +public class GaussDBMemberTranslatorProvider : RelationalMemberTranslatorProvider +{ + /// + /// 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 GaussDBJsonPocoTranslator JsonPocoTranslator { 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 GaussDBMemberTranslatorProvider( + RelationalMemberTranslatorProviderDependencies dependencies, + IModel model, + IRelationalTypeMappingSource typeMappingSource, + IDbContextOptions contextOptions) + : base(dependencies) + { + var npgsqlOptions = contextOptions.FindExtension() ?? new GaussDBOptionsExtension(); + var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14); + + var sqlExpressionFactory = (GaussDBExpressionFactory)dependencies.SqlExpressionFactory; + JsonPocoTranslator = new GaussDBJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model); + + AddTranslators( + [ + new GaussDBBigIntegerMemberTranslator(sqlExpressionFactory), + new GaussDBDateTimeMemberTranslator(typeMappingSource, sqlExpressionFactory), + new GaussDBJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBLTreeTranslator(typeMappingSource, sqlExpressionFactory, model), + JsonPocoTranslator, + new GaussDBRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), + new GaussDBStringMemberTranslator(sqlExpressionFactory), + new GaussDBTimeSpanMemberTranslator(sqlExpressionFactory) + ]); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMethodCallTranslatorProvider.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMethodCallTranslatorProvider.cs new file mode 100644 index 0000000000..27122cdd15 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMethodCallTranslatorProvider.cs @@ -0,0 +1,68 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBMethodCallTranslatorProvider : RelationalMethodCallTranslatorProvider +{ + /// + /// 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 GaussDBLTreeTranslator LTreeTranslator { 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 GaussDBMethodCallTranslatorProvider( + RelationalMethodCallTranslatorProviderDependencies dependencies, + IModel model, + IDbContextOptions contextOptions) + : base(dependencies) + { + var npgsqlOptions = contextOptions.FindExtension() ?? new GaussDBOptionsExtension(); + var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14); + var supportRegexCount = npgsqlOptions.PostgresVersion.AtLeast(15); + + var sqlExpressionFactory = (GaussDBExpressionFactory)dependencies.SqlExpressionFactory; + var typeMappingSource = (GaussDBTypeMappingSource)dependencies.RelationalTypeMappingSource; + var jsonTranslator = new GaussDBJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model); + LTreeTranslator = new GaussDBLTreeTranslator(typeMappingSource, sqlExpressionFactory, model); + + AddTranslators( + [ + new GaussDBArrayMethodTranslator(sqlExpressionFactory, jsonTranslator), + new GaussDBByteArrayMethodTranslator(sqlExpressionFactory), + new GaussDBConvertTranslator(sqlExpressionFactory), + new GaussDBDateTimeMethodTranslator(typeMappingSource, sqlExpressionFactory), + new GaussDBFullTextSearchMethodTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBFuzzyStringMatchMethodTranslator(sqlExpressionFactory), + new GaussDBJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBJsonDbFunctionsTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBLikeTranslator(sqlExpressionFactory), + LTreeTranslator, + new GaussDBMathTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBNetworkTranslator(typeMappingSource, sqlExpressionFactory, model), + new GaussDBGuidTranslator(sqlExpressionFactory, npgsqlOptions.PostgresVersion), + new GaussDBObjectToStringTranslator(typeMappingSource, sqlExpressionFactory), + new GaussDBRandomTranslator(sqlExpressionFactory), + new GaussDBRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), + new GaussDBRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount), + new GaussDBRowValueTranslator(sqlExpressionFactory), + new GaussDBStringMethodTranslator(typeMappingSource, sqlExpressionFactory), + new GaussDBTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model) + ]); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMiscAggregateMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMiscAggregateMethodTranslator.cs new file mode 100644 index 0000000000..f8a0288160 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBMiscAggregateMethodTranslator.cs @@ -0,0 +1,199 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBMiscAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private static readonly MethodInfo StringJoin + = typeof(string).GetRuntimeMethod(nameof(string.Join), [typeof(string), typeof(IEnumerable)])!; + + private static readonly MethodInfo StringConcat + = typeof(string).GetRuntimeMethod(nameof(string.Concat), [typeof(IEnumerable)])!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly 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 + /// 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 GaussDBMiscAggregateMethodTranslator( + GaussDBExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + _model = 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 SqlExpression? Translate( + MethodInfo method, + EnumerableExpression source, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Docs: https://www.postgresql.org/docs/current/functions-aggregate.html + + if (source.Selector is not SqlExpression sqlExpression) + { + return null; + } + + if (method == StringJoin || method == StringConcat) + { + // string_agg filters out nulls, but string.Join treats them as empty strings; coalesce unless we know we're aggregating over + // a non-nullable column. + if (sqlExpression is not ColumnExpression { IsNullable: false }) + { + sqlExpression = _sqlExpressionFactory.Coalesce( + sqlExpression, + _sqlExpressionFactory.Constant(string.Empty, typeof(string))); + } + + // string_agg returns null when there are no rows (or non-null values), but string.Join returns an empty string. + return _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.AggregateFunction( + "string_agg", + [ + sqlExpression, + method == StringJoin ? arguments[0] : _sqlExpressionFactory.Constant(string.Empty, typeof(string)) + ], + source, + nullable: 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))); + } + + if (method.DeclaringType == typeof(GaussDBAggregateDbFunctionsExtensions)) + { + switch (method.Name) + { + case nameof(GaussDBAggregateDbFunctionsExtensions.ArrayAgg): + return _sqlExpressionFactory.AggregateFunction( + "array_agg", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: method.ReturnType, + typeMapping: sqlExpression.TypeMapping is null + ? null + : _typeMappingSource.FindMapping(method.ReturnType, _model, sqlExpression.TypeMapping)); + + case nameof(GaussDBAggregateDbFunctionsExtensions.JsonAgg): + return _sqlExpressionFactory.AggregateFunction( + "json_agg", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: method.ReturnType, + _typeMappingSource.FindMapping(method.ReturnType, "json")); + + case nameof(GaussDBAggregateDbFunctionsExtensions.JsonbAgg): + return _sqlExpressionFactory.AggregateFunction( + "jsonb_agg", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: method.ReturnType, + _typeMappingSource.FindMapping(method.ReturnType, "jsonb")); + + case nameof(GaussDBAggregateDbFunctionsExtensions.Sum): + return _sqlExpressionFactory.AggregateFunction( + "sum", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, + sqlExpression.TypeMapping); + + case nameof(GaussDBAggregateDbFunctionsExtensions.Average): + return _sqlExpressionFactory.AggregateFunction( + "avg", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, + sqlExpression.TypeMapping); + + case nameof(GaussDBAggregateDbFunctionsExtensions.JsonbObjectAgg): + case nameof(GaussDBAggregateDbFunctionsExtensions.JsonObjectAgg): + var isJsonb = method.Name == nameof(GaussDBAggregateDbFunctionsExtensions.JsonbObjectAgg); + + // These methods accept two enumerable (column) arguments; this is represented in LINQ as a projection from the grouping + // to a tuple of the two columns. Since we generally translate tuples to PostgresRowValueExpression, we take it apart + // here. + if (source.Selector is not GaussDBRowValueExpression rowValueExpression) + { + return null; + } + + var (keys, values) = (rowValueExpression.Values[0], rowValueExpression.Values[1]); + + return _sqlExpressionFactory.AggregateFunction( + isJsonb ? "jsonb_object_agg" : "json_object_agg", + [keys, values], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[2], + returnType: method.ReturnType, + _typeMappingSource.FindMapping(method.ReturnType, isJsonb ? "jsonb" : "json")); + } + } + + if (method.DeclaringType == typeof(GaussDBCidrDbFunctionsExtensions)) + { + switch (method.Name) + { + case nameof(GaussDBCidrDbFunctionsExtensions.RangeAgg): + var arrayClrType = sqlExpression.Type.MakeArrayType(); + + return _sqlExpressionFactory.AggregateFunction( + "range_agg", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: arrayClrType, + _typeMappingSource.FindMapping(arrayClrType)); + + case nameof(GaussDBCidrDbFunctionsExtensions.RangeIntersectAgg): + return _sqlExpressionFactory.AggregateFunction( + "range_intersect_agg", + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: sqlExpression.Type, + sqlExpression.TypeMapping); + } + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBNetworkTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBNetworkTranslator.cs new file mode 100644 index 0000000000..e69606b24f --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBNetworkTranslator.cs @@ -0,0 +1,263 @@ +using System.Net; +using System.Net.NetworkInformation; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Provides translation services for operators and functions of GaussDB network typess (cidr, inet, macaddr, macaddr8). +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-net.html +/// +public class GaussDBNetworkTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo IPAddressParse = + typeof(IPAddress).GetRuntimeMethod(nameof(IPAddress.Parse), [typeof(string)])!; + + private static readonly MethodInfo PhysicalAddressParse = + typeof(PhysicalAddress).GetRuntimeMethod(nameof(PhysicalAddress.Parse), [typeof(string)])!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + + private readonly RelationalTypeMapping _inetMapping; + private readonly RelationalTypeMapping _cidrMapping; + private readonly RelationalTypeMapping _macaddr8Mapping; + private readonly RelationalTypeMapping _longAddressMapping; + + /// + /// 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 GaussDBNetworkTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _inetMapping = typeMappingSource.FindMapping("inet")!; + _cidrMapping = typeMappingSource.FindMapping("cidr")!; + _macaddr8Mapping = typeMappingSource.FindMapping("macaddr8")!; + _longAddressMapping = typeMappingSource.FindMapping(typeof(long), model)!; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == IPAddressParse) + { + return _sqlExpressionFactory.Convert(arguments[0], typeof(IPAddress)); + } + + if (method == PhysicalAddressParse) + { + return _sqlExpressionFactory.Convert(arguments[0], typeof(PhysicalAddress)); + } + + if (method.DeclaringType == typeof(GaussDBNetworkDbFunctionsExtensions)) + { + var paramType = method.GetParameters()[1].ParameterType; + + if (paramType == typeof(GaussDBInet)) + { + return TranslateInetExtensionMethod(method, arguments); + } + + if (paramType == typeof(Metadata.GaussDBRange)) + { + return TranslateCidrExtensionMethod(method, arguments); + } + + if (paramType == typeof(PhysicalAddress)) + { + return TranslateMacaddrExtensionMethod(method, arguments); + } + } + + return null; + } + + private SqlExpression? TranslateInetExtensionMethod(MethodInfo method, IReadOnlyList arguments) + => method.Name switch + { + nameof(GaussDBNetworkDbFunctionsExtensions.LessThan) + => new SqlBinaryExpression( + ExpressionType.LessThan, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.LessThanOrEqual) + => new SqlBinaryExpression( + ExpressionType.LessThanOrEqual, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.GreaterThanOrEqual) + => new SqlBinaryExpression( + ExpressionType.GreaterThanOrEqual, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.GreaterThan) + => new SqlBinaryExpression( + ExpressionType.GreaterThan, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.ContainedBy) + => _sqlExpressionFactory.ContainedBy(arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.ContainedByOrEqual) + => _sqlExpressionFactory.MakePostgresBinary( + GaussDBExpressionType.NetworkContainedByOrEqual, arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.Contains) + => _sqlExpressionFactory.Contains(arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.ContainsOrEqual) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.NetworkContainsOrEqual, arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.ContainsOrContainedBy) + => _sqlExpressionFactory.MakePostgresBinary( + GaussDBExpressionType.NetworkContainsOrContainedBy, arguments[1], arguments[2]), + + nameof(GaussDBNetworkDbFunctionsExtensions.BitwiseNot) + => new SqlUnaryExpression( + ExpressionType.Not, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.BitwiseAnd) + => new SqlBinaryExpression( + ExpressionType.And, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.BitwiseOr) + => new SqlBinaryExpression( + ExpressionType.Or, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.Add) + => new SqlBinaryExpression( + ExpressionType.Add, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.Subtract) when arguments[2].Type == typeof(long) + => new SqlBinaryExpression( + ExpressionType.Subtract, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(GaussDBInet), + _inetMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.Subtract) + => new SqlBinaryExpression( + ExpressionType.Subtract, + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + typeof(long), + _longAddressMapping), + + nameof(GaussDBNetworkDbFunctionsExtensions.Abbreviate) + => NullPropagatingFunction("abbrev", [arguments[1]], typeof(string)), + nameof(GaussDBNetworkDbFunctionsExtensions.Broadcast) + => NullPropagatingFunction("broadcast", [arguments[1]], typeof(IPAddress), _inetMapping), + nameof(GaussDBNetworkDbFunctionsExtensions.Family) + => NullPropagatingFunction("family", [arguments[1]], typeof(int)), + nameof(GaussDBNetworkDbFunctionsExtensions.Host) + => NullPropagatingFunction("host", [arguments[1]], typeof(string)), + nameof(GaussDBNetworkDbFunctionsExtensions.HostMask) + => NullPropagatingFunction("hostmask", [arguments[1]], typeof(IPAddress), _inetMapping), + nameof(GaussDBNetworkDbFunctionsExtensions.MaskLength) + => NullPropagatingFunction("masklen", [arguments[1]], typeof(int)), + nameof(GaussDBNetworkDbFunctionsExtensions.Netmask) + => NullPropagatingFunction("netmask", [arguments[1]], typeof(IPAddress), _inetMapping), + nameof(GaussDBNetworkDbFunctionsExtensions.Network) + => NullPropagatingFunction("network", [arguments[1]], typeof((IPAddress Address, int Subnet)), _cidrMapping), + nameof(GaussDBNetworkDbFunctionsExtensions.SetMaskLength) + => NullPropagatingFunction( + "set_masklen", [arguments[1], arguments[2]], arguments[1].Type, arguments[1].TypeMapping), + nameof(GaussDBNetworkDbFunctionsExtensions.Text) + => NullPropagatingFunction("text", [arguments[1]], typeof(string)), + nameof(GaussDBNetworkDbFunctionsExtensions.SameFamily) + => NullPropagatingFunction("inet_same_family", [arguments[1], arguments[2]], typeof(bool)), + nameof(GaussDBNetworkDbFunctionsExtensions.Merge) + => NullPropagatingFunction( + "inet_merge", [arguments[1], arguments[2]], typeof((IPAddress Address, int Subnet)), _cidrMapping), + + _ => null + }; + + private SqlExpression? TranslateCidrExtensionMethod(MethodInfo method, IReadOnlyList arguments) + => method.Name switch + { + nameof(GaussDBNetworkDbFunctionsExtensions.Abbreviate) + => NullPropagatingFunction("abbrev", [arguments[1]], typeof(string)), + nameof(GaussDBNetworkDbFunctionsExtensions.SetMaskLength) + => NullPropagatingFunction( + "set_masklen", [arguments[1], arguments[2]], arguments[1].Type, arguments[1].TypeMapping), + + _ => null + }; + + private SqlExpression? TranslateMacaddrExtensionMethod(MethodInfo method, IReadOnlyList arguments) + => method.Name switch + { + nameof(GaussDBNetworkDbFunctionsExtensions.LessThan) + => _sqlExpressionFactory.LessThan(arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.LessThanOrEqual) + => _sqlExpressionFactory.LessThanOrEqual(arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.GreaterThanOrEqual) + => _sqlExpressionFactory.GreaterThanOrEqual(arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.GreaterThan) + => _sqlExpressionFactory.GreaterThan(arguments[1], arguments[2]), + + nameof(GaussDBNetworkDbFunctionsExtensions.BitwiseNot) + => _sqlExpressionFactory.Not(arguments[1]), + nameof(GaussDBNetworkDbFunctionsExtensions.BitwiseAnd) + => _sqlExpressionFactory.And(arguments[1], arguments[2]), + nameof(GaussDBNetworkDbFunctionsExtensions.BitwiseOr) + => _sqlExpressionFactory.Or(arguments[1], arguments[2]), + + nameof(GaussDBNetworkDbFunctionsExtensions.Truncate) => NullPropagatingFunction( + "trunc", [arguments[1]], typeof(PhysicalAddress), arguments[1].TypeMapping), + nameof(GaussDBNetworkDbFunctionsExtensions.Set7BitMac8) => NullPropagatingFunction( + "macaddr8_set7bit", [arguments[1]], typeof(PhysicalAddress), _macaddr8Mapping), + + _ => null + }; + + private SqlExpression NullPropagatingFunction( + string name, + SqlExpression[] arguments, + Type returnType, + RelationalTypeMapping? typeMapping = null) + => _sqlExpressionFactory.Function( + name, + arguments, + nullable: true, + argumentsPropagateNullability: TrueArrays[arguments.Length], + returnType, + typeMapping); +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBObjectToStringTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBObjectToStringTranslator.cs new file mode 100644 index 0000000000..43f97aad1f --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBObjectToStringTranslator.cs @@ -0,0 +1,104 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBObjectToStringTranslator : IMethodCallTranslator +{ + private static readonly HashSet _typeMapping = + [ + typeof(int), + typeof(long), + typeof(DateTime), + typeof(Guid), + typeof(bool), + typeof(byte), + //typeof(byte[]) + typeof(double), + typeof(DateTimeOffset), + typeof(char), + typeof(short), + typeof(float), + typeof(decimal), + typeof(TimeSpan), + typeof(uint), + typeof(ushort), + typeof(ulong), + typeof(sbyte) + ]; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _textTypeMapping; + + /// + /// 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 GaussDBObjectToStringTranslator(IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + + _textTypeMapping = typeMappingSource.FindMapping("text")!; + } + + /// + /// 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? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (instance is null || method.Name != nameof(ToString) || arguments.Count != 0) + { + return null; + } + + if (instance.Type == typeof(bool)) + { + 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( + instance, + _sqlExpressionFactory.Constant(true.ToString())) + ], + _sqlExpressionFactory.Constant(false.ToString())); + } + } + + return _typeMapping.Contains(instance.Type) + || instance.Type.UnwrapNullableType().IsEnum && instance.TypeMapping is GaussDBEnumTypeMapping + ? _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Convert(instance, typeof(string), _textTypeMapping), + _sqlExpressionFactory.Constant(string.Empty)) + : null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBQueryableAggregateMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBQueryableAggregateMethodTranslator.cs new file mode 100644 index 0000000000..eac5b8efc4 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBQueryableAggregateMethodTranslator.cs @@ -0,0 +1,186 @@ +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBQueryableAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + + /// + /// 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 GaussDBQueryableAggregateMethodTranslator( + GaussDBExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + /// + /// 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? Translate( + MethodInfo method, + EnumerableExpression source, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(Queryable)) + { + var methodInfo = method.IsGenericMethod + ? method.GetGenericMethodDefinition() + : method; + switch (methodInfo.Name) + { + case nameof(Queryable.Average) + when (QueryableMethods.IsAverageWithoutSelector(methodInfo) + || QueryableMethods.IsAverageWithSelector(methodInfo)) + && source.Selector is SqlExpression averageSqlExpression: + var averageInputType = averageSqlExpression.Type; + if (averageInputType == typeof(int) + || averageInputType == typeof(long)) + { + averageSqlExpression = _sqlExpressionFactory.ApplyDefaultTypeMapping( + _sqlExpressionFactory.Convert(averageSqlExpression, typeof(double))); + } + + return averageInputType == typeof(float) + ? _sqlExpressionFactory.Convert( + _sqlExpressionFactory.AggregateFunction( + "avg", + [averageSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + returnType: typeof(double)), + averageSqlExpression.Type, + averageSqlExpression.TypeMapping) + : _sqlExpressionFactory.AggregateFunction( + "avg", + [averageSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + averageSqlExpression.Type, + averageSqlExpression.TypeMapping); + + // GaussDB COUNT() always returns bigint, so we need to downcast to int + case nameof(Queryable.Count) + when methodInfo == QueryableMethods.CountWithoutPredicate + || methodInfo == QueryableMethods.CountWithPredicate: + var countSqlExpression = (source.Selector as SqlExpression) ?? _sqlExpressionFactory.Fragment("*"); + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.AggregateFunction( + "count", + [countSqlExpression], + source, + nullable: false, + argumentsPropagateNullability: FalseArrays[1], + typeof(long)), + typeof(int), + _typeMappingSource.FindMapping(typeof(int))); + + case nameof(Queryable.LongCount) + when methodInfo == QueryableMethods.LongCountWithoutPredicate + || methodInfo == QueryableMethods.LongCountWithPredicate: + var longCountSqlExpression = (source.Selector as SqlExpression) ?? _sqlExpressionFactory.Fragment("*"); + return _sqlExpressionFactory.AggregateFunction( + "count", + [longCountSqlExpression], + source, + nullable: false, + argumentsPropagateNullability: FalseArrays[1], + typeof(long)); + + case nameof(Queryable.Max) + when (methodInfo == QueryableMethods.MaxWithoutSelector + || methodInfo == QueryableMethods.MaxWithSelector) + && source.Selector is SqlExpression maxSqlExpression: + return _sqlExpressionFactory.AggregateFunction( + "max", + [maxSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + maxSqlExpression.Type, + maxSqlExpression.TypeMapping); + + case nameof(Queryable.Min) + when (methodInfo == QueryableMethods.MinWithoutSelector + || methodInfo == QueryableMethods.MinWithSelector) + && source.Selector is SqlExpression minSqlExpression: + return _sqlExpressionFactory.AggregateFunction( + "min", + [minSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + minSqlExpression.Type, + minSqlExpression.TypeMapping); + + // In GaussDB SUM() doesn't return the same type as its argument for smallint, int and bigint. + // Cast to get the same type. + // http://www.postgresql.org/docs/current/static/functions-aggregate.html + case nameof(Queryable.Sum) + when (QueryableMethods.IsSumWithoutSelector(methodInfo) + || QueryableMethods.IsSumWithSelector(methodInfo)) + && source.Selector is SqlExpression sumSqlExpression: + var sumInputType = sumSqlExpression.Type; + + // Note that there is no Sum over short in LINQ + if (sumInputType == typeof(int)) + { + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.AggregateFunction( + "sum", + [sumSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + typeof(long)), + sumInputType, + sumSqlExpression.TypeMapping); + } + + if (sumInputType == typeof(long)) + { + return _sqlExpressionFactory.Convert( + _sqlExpressionFactory.AggregateFunction( + "sum", + [sumSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + typeof(decimal)), + sumInputType, + sumSqlExpression.TypeMapping); + } + + return _sqlExpressionFactory.AggregateFunction( + "sum", + [sumSqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + sumInputType, + sumSqlExpression.TypeMapping); + } + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRandomTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRandomTranslator.cs new file mode 100644 index 0000000000..4c5894f19d --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRandomTranslator.cs @@ -0,0 +1,52 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBRandomTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo _methodInfo + = typeof(DbFunctionsExtensions).GetRuntimeMethod(nameof(DbFunctionsExtensions.Random), [typeof(DbFunctions)])!; + + 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 GaussDBRandomTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + Check.NotNull(method, nameof(method)); + Check.NotNull(arguments, nameof(arguments)); + Check.NotNull(logger, nameof(logger)); + + return _methodInfo.Equals(method) + ? _sqlExpressionFactory.Function( + "random", + [], + nullable: false, + argumentsPropagateNullability: [], + method.ReturnType) + : null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRangeTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRangeTranslator.cs new file mode 100644 index 0000000000..bfa117d8e0 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRangeTranslator.cs @@ -0,0 +1,181 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBRangeTranslator : IMethodCallTranslator, IMemberTranslator +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IModel _model; + private readonly bool _supportsMultiranges; + + private static readonly MethodInfo EnumerableAnyWithoutPredicate = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1); + + /// + /// 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 GaussDBRangeTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory npgsqlSqlExpressionFactory, + IModel model, + bool supportsMultiranges) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = npgsqlSqlExpressionFactory; + _model = model; + _supportsMultiranges = supportsMultiranges; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Any() over multirange -> NOT isempty(). GaussDBRange has IsEmpty which is translated below. + if (_supportsMultiranges + && method.IsGenericMethod + && method.GetGenericMethodDefinition() == EnumerableAnyWithoutPredicate + && arguments[0].IsMultirange()) + { + return _sqlExpressionFactory.Not( + _sqlExpressionFactory.Function( + "isempty", + [arguments[0]], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(bool))); + } + + if (method.DeclaringType != typeof(GaussDBCidrDbFunctionsExtensions) + && (method.DeclaringType != typeof(GaussDBMultirangeDbFunctionsExtensions) || !_supportsMultiranges)) + { + return null; + } + + if (method.Name == nameof(GaussDBCidrDbFunctionsExtensions.Merge)) + { + if (method.DeclaringType == typeof(GaussDBCidrDbFunctionsExtensions)) + { + var inferredMapping = ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]); + + return _sqlExpressionFactory.Function( + "range_merge", + [ + _sqlExpressionFactory.ApplyTypeMapping(arguments[0], inferredMapping), + _sqlExpressionFactory.ApplyTypeMapping(arguments[1], inferredMapping) + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType, + inferredMapping); + } + + if (method.DeclaringType == typeof(GaussDBMultirangeDbFunctionsExtensions)) + { + var returnTypeMapping = arguments[0].TypeMapping is GaussDBMultirangeTypeMapping multirangeTypeMapping + ? multirangeTypeMapping.RangeMapping + : null; + + return _sqlExpressionFactory.Function( + "range_merge", + [arguments[0]], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + method.ReturnType, + returnTypeMapping); + } + } + + return method.Name switch + { + nameof(GaussDBCidrDbFunctionsExtensions.Contains) + => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.ContainedBy) + => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.Overlaps) + => _sqlExpressionFactory.Overlaps(arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.IsStrictlyLeftOf) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeIsStrictlyLeftOf, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.IsStrictlyRightOf) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeIsStrictlyRightOf, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.DoesNotExtendRightOf) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeDoesNotExtendRightOf, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.DoesNotExtendLeftOf) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeDoesNotExtendLeftOf, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.IsAdjacentTo) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeIsAdjacentTo, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.Union) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeUnion, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.Intersect) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeIntersect, arguments[0], arguments[1]), + nameof(GaussDBCidrDbFunctionsExtensions.Except) + => _sqlExpressionFactory.MakePostgresBinary(GaussDBExpressionType.RangeExcept, arguments[0], arguments[1]), + + _ => null + }; + } + + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + var type = member.DeclaringType; + if (type is null || !type.IsGenericType || type.GetGenericTypeDefinition() != typeof(GaussDBRange<>)) + { + return null; + } + + if (member.Name is nameof(GaussDBRange.LowerBound) or nameof(GaussDBRange.UpperBound)) + { + var typeMapping = instance!.TypeMapping is GaussDBRangeTypeMapping rangeMapping + ? rangeMapping.SubtypeMapping + : _typeMappingSource.FindMapping(returnType, _model); + + return _sqlExpressionFactory.Function( + member.Name == nameof(GaussDBRange.LowerBound) ? "lower" : "upper", + [instance], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + returnType, + typeMapping); + } + + return member.Name switch + { + nameof(GaussDBRange.IsEmpty) => SingleArgBoolFunction("isempty", instance!), + nameof(GaussDBRange.LowerBoundIsInclusive) => SingleArgBoolFunction("lower_inc", instance!), + nameof(GaussDBRange.UpperBoundIsInclusive) => SingleArgBoolFunction("upper_inc", instance!), + nameof(GaussDBRange.LowerBoundInfinite) => SingleArgBoolFunction("lower_inf", instance!), + nameof(GaussDBRange.UpperBoundInfinite) => SingleArgBoolFunction("upper_inf", instance!), + + _ => null + }; + + SqlExpression SingleArgBoolFunction(string name, SqlExpression argument) + => _sqlExpressionFactory.Function( + name, + [argument], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(bool)); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRegexTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRegexTranslator.cs new file mode 100644 index 0000000000..01cab6f1f0 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRegexTranslator.cs @@ -0,0 +1,193 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Translates Regex method calls into their corresponding GaussDB equivalent for database-side processing. +/// +/// +/// http://www.postgresql.org/docs/current/static/functions-matching.html +/// +public class GaussDBRegexTranslator : IMethodCallTranslator +{ + private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly bool _supportRegexCount; + private readonly GaussDBTypeMappingSource _typeMappingSource; + + /// + /// 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 GaussDBRegexTranslator( + GaussDBTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + bool supportRegexCount) + { + _sqlExpressionFactory = sqlExpressionFactory; + _supportRegexCount = supportRegexCount; + _typeMappingSource = typeMappingSource; + } + + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType != typeof(Regex) || !method.IsStatic) + { + return null; + } + + return method.Name switch + { + nameof(Regex.IsMatch) when arguments.Count == 2 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + => TranslateIsMatch(arguments), + nameof(Regex.IsMatch) when arguments.Count == 3 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && TryGetOptions(arguments[2], out var options) + => TranslateIsMatch(arguments, options), + + nameof(Regex.Replace) when arguments.Count == 3 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && arguments[2].Type == typeof(string) + => TranslateReplace(arguments), + nameof(Regex.Replace) when arguments.Count == 4 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && arguments[2].Type == typeof(string) + && TryGetOptions(arguments[3], out var options) + => TranslateReplace(arguments, options), + + nameof(Regex.Count) when _supportRegexCount + && arguments.Count == 2 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + => TranslateCount(arguments), + nameof(Regex.Count) when _supportRegexCount + && arguments.Count == 3 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && TryGetOptions(arguments[2], out var options) + => TranslateCount(arguments, options), + + _ => null + }; + + static bool TryGetOptions(SqlExpression argument, out RegexOptions options) + { + if (argument is SqlConstantExpression { Value: RegexOptions o } && (o & UnsupportedRegexOptions) is 0) + { + options = o; + return true; + } + + options = default; + return false; + } + } + + private GaussDBRegexMatchExpression TranslateIsMatch(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) + => _sqlExpressionFactory.RegexMatch( + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + regexOptions); + + private SqlExpression TranslateReplace(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) + { + var (input, pattern, replacement) = (arguments[0], arguments[1], arguments[2]); + + List passingArguments = + [ + _sqlExpressionFactory.ApplyDefaultTypeMapping(input), + _sqlExpressionFactory.ApplyDefaultTypeMapping(pattern), + _sqlExpressionFactory.ApplyDefaultTypeMapping(replacement) + ]; + + if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions) + { + passingArguments.Add(_sqlExpressionFactory.Constant(translatedOptions)); + } + + return _sqlExpressionFactory.Function( + "regexp_replace", + passingArguments, + nullable: true, + TrueArrays[passingArguments.Count], + typeof(string), + _typeMappingSource.FindMapping(typeof(string))); + } + + private SqlExpression TranslateCount(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) + { + var (input, pattern) = (arguments[0], arguments[1]); + + List passingArguments = + [ + _sqlExpressionFactory.ApplyDefaultTypeMapping(input), + _sqlExpressionFactory.ApplyDefaultTypeMapping(pattern) + ]; + + if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions) + { + passingArguments.AddRange( + [ + _sqlExpressionFactory.Constant(1), // The starting position has to be set to use the options in GaussDB + _sqlExpressionFactory.Constant(translatedOptions) + ]); + } + + return _sqlExpressionFactory.Function( + "regexp_count", + passingArguments, + nullable: true, + TrueArrays[passingArguments.Count], + typeof(int), + _typeMappingSource.FindMapping(typeof(int))); + } + + private static string TranslateOptions(RegexOptions options) + { + string? result; + + switch (options) + { + case RegexOptions.Singleline: + return string.Empty; + case var _ when options.HasFlag(RegexOptions.Multiline): + result = "n"; + break; + case var _ when !options.HasFlag(RegexOptions.Singleline): + result = "p"; + break; + default: + result = string.Empty; + break; + } + + if (options.HasFlag(RegexOptions.IgnoreCase)) + { + result += "i"; + } + + if (options.HasFlag(RegexOptions.IgnorePatternWhitespace)) + { + result += "x"; + } + + return result; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRowValueTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRowValueTranslator.cs new file mode 100644 index 0000000000..74e4749f0b --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBRowValueTranslator.cs @@ -0,0 +1,95 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBRowValueTranslator : IMethodCallTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + + private static readonly MethodInfo GreaterThan = + typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.GreaterThan), + [typeof(DbFunctions), typeof(ITuple), typeof(ITuple)])!; + + private static readonly MethodInfo LessThan = + typeof(GaussDBDbFunctionsExtensions).GetMethods() + .Single(m => m.Name == nameof(GaussDBDbFunctionsExtensions.LessThan)); + + private static readonly MethodInfo GreaterThanOrEqual = + typeof(GaussDBDbFunctionsExtensions).GetMethods() + .Single(m => m.Name == nameof(GaussDBDbFunctionsExtensions.GreaterThanOrEqual)); + + private static readonly MethodInfo LessThanOrEqual = + typeof(GaussDBDbFunctionsExtensions).GetMethods() + .Single(m => m.Name == nameof(GaussDBDbFunctionsExtensions.LessThanOrEqual)); + + private static readonly Dictionary ComparisonMethods = new() + { + { GreaterThan, ExpressionType.GreaterThan }, + { LessThan, ExpressionType.LessThan }, + { GreaterThanOrEqual, ExpressionType.GreaterThanOrEqual }, + { LessThanOrEqual, ExpressionType.LessThanOrEqual } + }; + + /// + /// Initializes a new instance of the class. + /// + public GaussDBRowValueTranslator(GaussDBExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(ValueType))] // For ValueTuple.Create + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Translate ValueTuple.Create + if (method.DeclaringType == typeof(ValueTuple) && method is { IsStatic: true, Name: nameof(ValueTuple.Create) }) + { + return new GaussDBRowValueExpression(arguments, method.ReturnType); + } + + // Translate EF.Functions.GreaterThan and other comparisons + if (method.DeclaringType != typeof(GaussDBDbFunctionsExtensions) || !ComparisonMethods.TryGetValue(method, out var expressionType)) + { + return null; + } + + var leftCount = arguments[1] is GaussDBRowValueExpression leftRowValue + ? leftRowValue.Values.Count + : arguments[1] is SqlConstantExpression { Value : ITuple leftTuple } + ? (int?)leftTuple.Length + : null; + + var rightCount = arguments[2] is GaussDBRowValueExpression rightRowValue + ? rightRowValue.Values.Count + : arguments[2] is SqlConstantExpression { Value : ITuple rightTuple } + ? (int?)rightTuple.Length + : null; + + if (leftCount is null || rightCount is null) + { + return null; + } + + if (leftCount != rightCount) + { + throw new ArgumentException(GaussDBStrings.RowValueComparisonRequiresTuplesOfSameLength); + } + + return _sqlExpressionFactory.MakeBinary(expressionType, arguments[1], arguments[2], typeMapping: null); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStatisticsAggregateMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStatisticsAggregateMethodTranslator.cs new file mode 100644 index 0000000000..097daa5cd7 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStatisticsAggregateMethodTranslator.cs @@ -0,0 +1,122 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBStatisticsAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _doubleTypeMapping, _longTypeMapping; + + /// + /// 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 GaussDBStatisticsAggregateMethodTranslator( + GaussDBExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!; + _longTypeMapping = typeMappingSource.FindMapping(typeof(long))!; + } + + /// + /// 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? Translate( + MethodInfo method, + EnumerableExpression source, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Docs: https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-STATISTICS-TABLE + + if (method.DeclaringType != typeof(GaussDBAggregateDbFunctionsExtensions) + || source.Selector is not SqlExpression sqlExpression) + { + return null; + } + + // These four functions are simple and take a single enumerable argument + var functionName = method.Name switch + { + nameof(GaussDBAggregateDbFunctionsExtensions.StandardDeviationSample) => "stddev_samp", + nameof(GaussDBAggregateDbFunctionsExtensions.StandardDeviationPopulation) => "stddev_pop", + nameof(GaussDBAggregateDbFunctionsExtensions.VarianceSample) => "var_samp", + nameof(GaussDBAggregateDbFunctionsExtensions.VariancePopulation) => "var_pop", + _ => null + }; + + if (functionName is not null) + { + return _sqlExpressionFactory.AggregateFunction( + functionName, + [sqlExpression], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[1], + typeof(double), + _doubleTypeMapping); + } + + functionName = method.Name switch + { + nameof(GaussDBAggregateDbFunctionsExtensions.Correlation) => "corr", + nameof(GaussDBAggregateDbFunctionsExtensions.CovariancePopulation) => "covar_pop", + nameof(GaussDBAggregateDbFunctionsExtensions.CovarianceSample) => "covar_samp", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrAverageX) => "regr_avgx", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrAverageY) => "regr_avgy", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrCount) => "regr_count", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrIntercept) => "regr_intercept", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrR2) => "regr_r2", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrSlope) => "regr_slope", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrSXX) => "regr_sxx", + nameof(GaussDBAggregateDbFunctionsExtensions.RegrSXY) => "regr_sxy", + _ => null + }; + + if (functionName is not null) + { + // These methods accept two enumerable (column) arguments; this is represented in LINQ as a projection from the grouping + // to a tuple of the two columns. Since we generally translate tuples to PostgresRowValueExpression, we take it apart here. + if (source.Selector is not GaussDBRowValueExpression rowValueExpression) + { + return null; + } + + var (y, x) = (rowValueExpression.Values[0], rowValueExpression.Values[1]); + + return method.Name == nameof(GaussDBAggregateDbFunctionsExtensions.RegrCount) + ? _sqlExpressionFactory.AggregateFunction( + functionName, + [y, x], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[2], + typeof(long), + _longTypeMapping) + : _sqlExpressionFactory.AggregateFunction( + functionName, + [y, x], + source, + nullable: true, + argumentsPropagateNullability: FalseArrays[2], + typeof(double), + _doubleTypeMapping); + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStringMemberTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStringMemberTranslator.cs new file mode 100644 index 0000000000..df93186cf4 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStringMemberTranslator.cs @@ -0,0 +1,44 @@ +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Translates to 'length(text)'. +/// +public class GaussDBStringMemberTranslator : IMemberTranslator +{ + 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 GaussDBStringMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + => member.Name == nameof(string.Length) && member.DeclaringType == typeof(string) + ? _sqlExpressionFactory.Convert( + _sqlExpressionFactory.Function( + "length", + [instance!], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(long)), + returnType) + : null; +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStringMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStringMethodTranslator.cs new file mode 100644 index 0000000000..51bc88e547 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBStringMethodTranslator.cs @@ -0,0 +1,461 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; + +/// +/// Provides translation services for GaussDB string functions. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-string.html +/// +public class GaussDBStringMethodTranslator : IMethodCallTranslator +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly SqlExpression _whitespace; + + #region MethodInfo + + 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), [typeof(string)])!; + + private static readonly MethodInfo PadLeft = typeof(string).GetRuntimeMethod(nameof(string.PadLeft), [typeof(int)])!; + + private static readonly MethodInfo PadLeftWithChar = typeof(string).GetRuntimeMethod( + nameof(string.PadLeft), [typeof(int), typeof(char)])!; + + private static readonly MethodInfo PadRight = typeof(string).GetRuntimeMethod(nameof(string.PadRight), [typeof(int)])!; + + private static readonly MethodInfo PadRightWithChar = typeof(string).GetRuntimeMethod( + nameof(string.PadRight), [typeof(int), typeof(char)])!; + + private static readonly MethodInfo Replace = typeof(string).GetRuntimeMethod( + 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); + + 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), [])!; + 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), [typeof(char[])])!; + + private static readonly MethodInfo TrimBothWithSingleChar = + 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), [typeof(char[])])!; + + private static readonly MethodInfo TrimEndWithSingleChar = + 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), [typeof(char[])])!; + + private static readonly MethodInfo TrimStartWithSingleChar = + typeof(string).GetRuntimeMethod(nameof(string.TrimStart), [typeof(char)])!; + + private static readonly MethodInfo Reverse = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.Reverse), [typeof(DbFunctions), typeof(string)])!; + + private static readonly MethodInfo StringToArray = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.StringToArray), [typeof(DbFunctions), typeof(string), typeof(string)])!; + + private static readonly MethodInfo StringToArrayNullString = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.StringToArray), [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; + + private static readonly MethodInfo ToDate = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.ToDate), [typeof(DbFunctions), typeof(string), typeof(string)])!; + + private static readonly MethodInfo ToTimestamp = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.ToTimestamp), [typeof(DbFunctions), typeof(string), typeof(string)])!; + + private static readonly MethodInfo FirstOrDefaultMethodInfoWithoutArgs + = typeof(Enumerable).GetRuntimeMethods().Single( + m => m.Name == nameof(Enumerable.FirstOrDefault) + && m.GetParameters().Length == 1).MakeGenericMethod(typeof(char)); + + private static readonly MethodInfo LastOrDefaultMethodInfoWithoutArgs + = typeof(Enumerable).GetRuntimeMethods().Single( + m => m.Name == nameof(Enumerable.LastOrDefault) + && m.GetParameters().Length == 1).MakeGenericMethod(typeof(char)); + + // ReSharper disable InconsistentNaming + private static readonly MethodInfo String_Join1 = + typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(object[])])!; + + private static readonly MethodInfo String_Join2 = + typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(string[])])!; + + private static readonly MethodInfo String_Join3 = + typeof(string).GetMethod(nameof(string.Join), [typeof(char), typeof(object[])])!; + + private static readonly MethodInfo String_Join4 = + 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) + .Single( + m => m is { Name: nameof(string.Join), IsGenericMethod: true } + && m.GetParameters().Length == 2 + && m.GetParameters()[0].ParameterType == typeof(string)); + + private static readonly MethodInfo String_Join_generic2 = + typeof(string).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single( + m => m is { Name: nameof(string.Join), IsGenericMethod: true } + && m.GetParameters().Length == 2 + && m.GetParameters()[0].ParameterType == typeof(char)); + // ReSharper restore InconsistentNaming + + #endregion + + /// + /// 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 GaussDBStringMethodTranslator(GaussDBTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) + { + _typeMappingSource = typeMappingSource; + _sqlExpressionFactory = sqlExpressionFactory; + _whitespace = _sqlExpressionFactory.Constant( + @" \t\n\r", // TODO: Complete this + typeMappingSource.EStringTypeMapping); + } + + /// + /// 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? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(string)) + { + return TranslateStringMethod(instance, method, arguments); + } + + if (method.DeclaringType == typeof(GaussDBDbFunctionsExtensions)) + { + return TranslateDbFunctionsMethod(instance, method, arguments); + } + + if (method == FirstOrDefaultMethodInfoWithoutArgs) + { + var argument = arguments[0]; + return _sqlExpressionFactory.Function( + "substr", + [argument, _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1)], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + method.ReturnType); + } + + if (method == LastOrDefaultMethodInfoWithoutArgs) + { + var argument = arguments[0]; + return _sqlExpressionFactory.Function( + "substr", + [ + argument, + _sqlExpressionFactory.Function( + "length", + [argument], + nullable: true, + argumentsPropagateNullability: [true], + typeof(int)), + _sqlExpressionFactory.Constant(1) + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + method.ReturnType); + } + + return null; + } + + private SqlExpression? TranslateStringMethod(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments) + { + if (method == IndexOfString || method == IndexOfChar) + { + var argument = arguments[0]; + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance!, argument); + + return _sqlExpressionFactory.Subtract( + _sqlExpressionFactory.Function( + "strpos", + [ + _sqlExpressionFactory.ApplyTypeMapping(instance!, stringTypeMapping), + _sqlExpressionFactory.ApplyTypeMapping(argument, stringTypeMapping) + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + method.ReturnType), + _sqlExpressionFactory.Constant(1)); + } + + if (method == Replace) + { + var oldValue = arguments[0]; + var newValue = arguments[1]; + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance!, oldValue, newValue); + + return _sqlExpressionFactory.Function( + "replace", + [ + _sqlExpressionFactory.ApplyTypeMapping(instance!, stringTypeMapping), + _sqlExpressionFactory.ApplyTypeMapping(oldValue, stringTypeMapping), + _sqlExpressionFactory.ApplyTypeMapping(newValue, stringTypeMapping) + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + method.ReturnType, + stringTypeMapping); + } + + if (method == ToLower || method == ToUpper) + { + return _sqlExpressionFactory.Function( + method == ToLower ? "lower" : "upper", + [instance!], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + method.ReturnType, + instance!.TypeMapping); + } + + if (method == Substring || method == SubstringWithLength) + { + var args = + method == Substring + ? [instance!, GenerateOneBasedIndexExpression(arguments[0])] + : new[] { instance!, GenerateOneBasedIndexExpression(arguments[0]), arguments[1] }; + return _sqlExpressionFactory.Function( + "substring", + args, + nullable: true, + argumentsPropagateNullability: TrueArrays[args.Length], + method.ReturnType, + instance!.TypeMapping); + } + + if (method == IsNullOrWhiteSpace) + { + var argument = arguments[0]; + + return _sqlExpressionFactory.OrElse( + _sqlExpressionFactory.IsNull(argument), + _sqlExpressionFactory.Equal( + _sqlExpressionFactory.Function( + "btrim", + [argument, _whitespace], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + argument.Type, + argument.TypeMapping), + _sqlExpressionFactory.Constant(string.Empty, argument.TypeMapping))); + } + + var isTrimStart = method == TrimStartWithNoParam || method == TrimStartWithChars || method == TrimStartWithSingleChar; + var isTrimEnd = method == TrimEndWithNoParam || method == TrimEndWithChars || method == TrimEndWithSingleChar; + var isTrimBoth = method == TrimBothWithNoParam || method == TrimBothWithChars || method == TrimBothWithSingleChar; + if (isTrimStart || isTrimEnd || isTrimBoth) + { + char[]? trimChars = null; + + if (method == TrimStartWithChars + || method == TrimStartWithSingleChar + || method == TrimEndWithChars + || method == TrimEndWithSingleChar + || method == TrimBothWithChars + || method == TrimBothWithSingleChar) + { + var constantTrimChars = arguments[0] as SqlConstantExpression; + if (constantTrimChars is null) + { + return null; // Don't translate if trim chars isn't a constant + } + + trimChars = constantTrimChars.Value is char c + ? [c] + : (char[]?)constantTrimChars.Value; + } + + return _sqlExpressionFactory.Function( + isTrimStart ? "ltrim" : isTrimEnd ? "rtrim" : "btrim", + [ + instance!, + trimChars is null || trimChars.Length == 0 + ? _whitespace + : _sqlExpressionFactory.Constant(new string(trimChars)) + ], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + instance!.Type, + instance.TypeMapping); + } + + if (method == PadLeft || method == PadLeftWithChar || method == PadRight || method == PadRightWithChar) + { + var args = + method == PadLeft || method == PadRight + ? [instance!, arguments[0]] + : new[] { instance!, arguments[0], arguments[1] }; + + return _sqlExpressionFactory.Function( + method == PadLeft || method == PadLeftWithChar ? "lpad" : "rpad", + args, + nullable: true, + argumentsPropagateNullability: TrueArrays[args.Length], + instance!.Type, + instance.TypeMapping); + } + + if (method.DeclaringType == typeof(string) + && (method == String_Join1 + || method == String_Join2 + || method == String_Join3 + || method == String_Join4 + || method == String_Join5 + || method.IsClosedFormOf(String_Join_generic1) + || method.IsClosedFormOf(String_Join_generic2)) + && arguments[1].TypeMapping is GaussDBArrayTypeMapping) + { + // 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. + if (arguments[1] is GaussDBNewArrayExpression newArrayExpression) + { + var rewrittenArguments = new SqlExpression[newArrayExpression.Expressions.Count + 1]; + rewrittenArguments[0] = arguments[0]; + + for (var i = 0; i < newArrayExpression.Expressions.Count; i++) + { + var argument = newArrayExpression.Expressions[i]; + + rewrittenArguments[i + 1] = argument switch + { + ColumnExpression { IsNullable: false } => argument, + SqlConstantExpression constantExpression => constantExpression.Value is null + ? _sqlExpressionFactory.Constant(string.Empty, typeof(string)) + : constantExpression, + _ => _sqlExpressionFactory.Coalesce(argument, _sqlExpressionFactory.Constant(string.Empty, typeof(string))) + }; + } + + // Only the delimiter (first arg) propagates nullability - all others are non-nullable, since we wrap the others in coalesce + // (where needed). + var argumentsPropagateNullability = new bool[rewrittenArguments.Length]; + argumentsPropagateNullability[0] = true; + + return _sqlExpressionFactory.Function( + "concat_ws", + rewrittenArguments, + nullable: true, + argumentsPropagateNullability, + typeof(string)); + } + + return _sqlExpressionFactory.Function( + "array_to_string", + [arguments[1], arguments[0], _sqlExpressionFactory.Constant("")], + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + typeof(string)); + } + + return null; + } + + private SqlExpression? TranslateDbFunctionsMethod(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments) + { + if (method == Reverse) + { + return _sqlExpressionFactory.Function( + "reverse", + [arguments[1]], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(string), + arguments[1].TypeMapping); + } + + if (method == StringToArray) + { + // Note that string_to_array always returns text[], regardless of the input type + return _sqlExpressionFactory.Function( + "string_to_array", + [arguments[1], arguments[2]], + nullable: true, + argumentsPropagateNullability: [true, false], + typeof(string[]), + _typeMappingSource.FindMapping(typeof(string[]))); + } + + if (method == StringToArrayNullString) + { + // Note that string_to_array always returns text[], regardless of the input type + return _sqlExpressionFactory.Function( + "string_to_array", + [arguments[1], arguments[2], arguments[3]], + nullable: true, + 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; + } + + private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) + => expression is SqlConstantExpression constant + ? _sqlExpressionFactory.Constant(Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) + : _sqlExpressionFactory.Add(expression, _sqlExpressionFactory.Constant(1)); +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBTimeSpanMemberTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBTimeSpanMemberTranslator.cs new file mode 100644 index 0000000000..9e29df7e4f --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBTimeSpanMemberTranslator.cs @@ -0,0 +1,87 @@ +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBTimeSpanMemberTranslator : IMemberTranslator +{ + 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 GaussDBTimeSpanMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + private static readonly bool[] FalseTrueArray = [false, 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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + Check.NotNull(member, nameof(member)); + Check.NotNull(returnType, nameof(returnType)); + + if (member.DeclaringType == typeof(TimeSpan) && instance is not null) + { + return member.Name switch + { + nameof(TimeSpan.Days) => Floor(DatePart("day", instance)), + nameof(TimeSpan.Hours) => Floor(DatePart("hour", instance)), + nameof(TimeSpan.Minutes) => Floor(DatePart("minute", instance)), + nameof(TimeSpan.Seconds) => Floor(DatePart("second", instance)), + nameof(TimeSpan.Milliseconds) => _sqlExpressionFactory.Modulo( + Floor(DatePart("millisecond", instance)), + _sqlExpressionFactory.Constant(1000)), + + nameof(TimeSpan.TotalDays) => TranslateDurationTotalMember(instance, 86400), + nameof(TimeSpan.TotalHours) => TranslateDurationTotalMember(instance, 3600), + nameof(TimeSpan.TotalMinutes) => TranslateDurationTotalMember(instance, 60), + nameof(TimeSpan.TotalSeconds) => DatePart("epoch", instance), + nameof(TimeSpan.TotalMilliseconds) => TranslateDurationTotalMember(instance, 0.001), + + _ => null + }; + } + + return null; + + SqlExpression Floor(SqlExpression value) + => _sqlExpressionFactory.Convert( + _sqlExpressionFactory.Function( + "floor", + [value], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(double)), + typeof(int)); + + SqlExpression DatePart(string part, SqlExpression value) + => _sqlExpressionFactory.Function( + "date_part", [_sqlExpressionFactory.Constant(part), value], + nullable: true, + argumentsPropagateNullability: FalseTrueArray, + returnType); + + SqlExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) + => _sqlExpressionFactory.Divide(DatePart("epoch", instance), _sqlExpressionFactory.Constant(divisor)); + } +} diff --git a/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBTrigramsMethodTranslator.cs b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBTrigramsMethodTranslator.cs new file mode 100644 index 0000000000..5254df7dc4 --- /dev/null +++ b/src/EFCore.GaussDB/Query/ExpressionTranslators/Internal/GaussDBTrigramsMethodTranslator.cs @@ -0,0 +1,119 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.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 GaussDBTrigramsMethodTranslator : IMethodCallTranslator +{ + private static readonly Dictionary Functions = new() + { + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsShow), typeof(DbFunctions), typeof(string))] + = "show_trgm", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsSimilarity), typeof(DbFunctions), typeof(string), typeof(string))] + = "similarity", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsWordSimilarity), typeof(DbFunctions), typeof(string), typeof(string))] + = "word_similarity", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsStrictWordSimilarity), typeof(DbFunctions), typeof(string), typeof(string))] + = "strict_word_similarity" + }; + + private static readonly Dictionary BoolReturningOperators = new() + { + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsAreSimilar), typeof(DbFunctions), typeof(string), typeof(string))] + = "%", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsAreWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] + = "<%", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsAreNotWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] + = "%>", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsAreStrictWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] + = "<<%", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsAreNotStrictWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] + = "%>>" + }; + + private static readonly Dictionary FloatReturningOperators = new() + { + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsSimilarityDistance), typeof(DbFunctions), typeof(string), typeof(string))] + = "<->", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsWordSimilarityDistance), typeof(DbFunctions), typeof(string), typeof(string))] + = "<<->", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsWordSimilarityDistanceInverted), typeof(DbFunctions), typeof(string), typeof(string))] + = "<->>", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsStrictWordSimilarityDistance), typeof(DbFunctions), typeof(string), typeof(string))] + = "<<<->", + [GetRuntimeMethod(nameof(GaussDBTrigramsDbFunctionsExtensions.TrigramsStrictWordSimilarityDistanceInverted), typeof(DbFunctions), typeof(string), typeof(string))] + = "<->>>" + }; + + private static MethodInfo GetRuntimeMethod(string name, params Type[] parameters) + => typeof(GaussDBTrigramsDbFunctionsExtensions).GetRuntimeMethod(name, parameters)!; + + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _boolMapping; + private readonly RelationalTypeMapping _floatMapping; + + private static readonly bool[][] TrueArrays = [[], [true], [true, 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. + /// + public GaussDBTrigramsMethodTranslator( + IRelationalTypeMappingSource typeMappingSource, + GaussDBExpressionFactory sqlExpressionFactory, + IModel model) + { + _sqlExpressionFactory = sqlExpressionFactory; + _boolMapping = typeMappingSource.FindMapping(typeof(bool), model)!; + _floatMapping = typeMappingSource.FindMapping(typeof(float), model)!; + } + +#pragma warning disable EF1001 + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (Functions.TryGetValue(method, out var function)) + { + return _sqlExpressionFactory.Function( + function, + arguments.Skip(1), + nullable: true, + argumentsPropagateNullability: TrueArrays[arguments.Count - 1], + method.ReturnType); + } + + if (BoolReturningOperators.TryGetValue(method, out var boolOperator)) + { + return new GaussDBUnknownBinaryExpression( + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + boolOperator, + _boolMapping.ClrType, + _boolMapping); + } + + if (FloatReturningOperators.TryGetValue(method, out var floatOperator)) + { + return new GaussDBUnknownBinaryExpression( + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), + floatOperator, + _floatMapping.ClrType, + _floatMapping); + } + + return null; + } +#pragma warning restore EF1001 +} diff --git a/src/EFCore.GaussDB/Query/Expressions/GaussDBExpressionType.cs b/src/EFCore.GaussDB/Query/Expressions/GaussDBExpressionType.cs new file mode 100644 index 0000000000..a27362afb0 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/GaussDBExpressionType.cs @@ -0,0 +1,162 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; + +/// +/// GaussDB-specific expression node types. +/// +public enum GaussDBExpressionType +{ + #region General operators + + /// + /// Represents a GaussDB contains operator. + /// + Contains, // >> (inet/cidr), @> + + /// + /// Represents a GaussDB contained-by operator. + /// + ContainedBy, // << (inet/cidr), <@ + + /// + /// Represents a GaussDB overlap operator. + /// + Overlaps, // && + + /// + /// Represents a GaussDB operator for finding the distance between two things (e.g. 2D distance between two geometries, + /// between timestamps...) + /// + Distance, // <-> + + #endregion General operators + + #region Network + + /// + /// Represents a GaussDB network contained-by-or-equal operator. + /// + NetworkContainedByOrEqual, // <<= + + /// + /// Represents a GaussDB network contains-or-equal operator. + /// + NetworkContainsOrEqual, // >>= + + /// + /// Represents a GaussDB network contains-or-contained-by operator. + /// + NetworkContainsOrContainedBy, // && + + #endregion Network + + #region Range + + /// + /// Represents a GaussDB operator for checking if a range is strictly to the left of another range. + /// + RangeIsStrictlyLeftOf, // << + + /// + /// Represents a GaussDB operator for checking if a range is strictly to the right of another range. + /// + RangeIsStrictlyRightOf, // >> + + /// + /// Represents a GaussDB operator for checking if a range does not extend to the right of another range. + /// + RangeDoesNotExtendRightOf, // &< + + /// + /// Represents a GaussDB operator for checking if a range does not extend to the left of another range. + /// + RangeDoesNotExtendLeftOf, // &> + + /// + /// Represents a GaussDB operator for checking if a range is adjacent to another range. + /// + RangeIsAdjacentTo, // -|- + + /// + /// Represents a GaussDB operator for performing a union between two ranges. + /// + RangeUnion, // + + + /// + /// Represents a GaussDB operator for performing an intersection between two ranges. + /// + RangeIntersect, // * + + /// + /// Represents a GaussDB operator for performing an except operation between two ranges. + /// + RangeExcept, // - + + #endregion Range + + #region Text search + + /// + /// Represents a GaussDB operator for performing a full-text search match. + /// + TextSearchMatch, // @@ + + /// + /// Represents a GaussDB operator for logical AND within a full-text search match. + /// + TextSearchAnd, // && + + /// + /// Represents a GaussDB operator for logical OR within a full-text search match. + /// + TextSearchOr, // || + + #endregion Text search + + #region JSON + + /// + /// Represents a GaussDB operator for checking whether a key exists in a JSON document. + /// + JsonExists, // ? + + /// + /// Represents a GaussDB operator for checking whether any of multiple keys exists in a JSON document. + /// + JsonExistsAny, // ?@> + + /// + /// Represents a GaussDB operator for checking whether all the given keys exist in a JSON document. + /// + JsonExistsAll, // ?<@ + + #endregion JSON + + #region LTree + + /// + /// Represents a GaussDB operator for matching in an ltree type. + /// + LTreeMatches, // ~ or @ + + /// + /// Represents a GaussDB operator for matching in an ltree type. + /// + LTreeMatchesAny, // ? + + /// + /// Represents a GaussDB operator for finding the first ancestor in an ltree type. + /// + LTreeFirstAncestor, // ?@> + + /// + /// Represents a GaussDB operator for finding the first descendent in an ltree type. + /// + LTreeFirstDescendent, // ?<@ + + /// + /// Represents a GaussDB operator for finding the first match in an ltree type. + /// + LTreeFirstMatches, // ?~ or ?@ + + #endregion LTree +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBAllExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBAllExpression.cs new file mode 100644 index 0000000000..5e7771523f --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBAllExpression.cs @@ -0,0 +1,137 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents a GaussDB array ALL expression. +/// +/// +/// See https://www.postgresql.org/docs/current/static/functions-comparisons.html +/// +public class GaussDBAllExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + public override Type Type + => typeof(bool); + + /// + /// The value to test against the . + /// + public virtual SqlExpression Item { get; } + + /// + /// The array of values or patterns to test for the . + /// + public virtual SqlExpression Array { get; } + + /// + /// The operator. + /// + public virtual PgAllOperatorType OperatorType { get; } + + /// + /// Constructs a . + /// + /// The operator symbol to the array expression. + /// The value to find. + /// The array to search. + /// The type mapping for the expression. + public GaussDBAllExpression( + SqlExpression item, + SqlExpression array, + PgAllOperatorType operatorType, + RelationalTypeMapping? typeMapping) + : base(typeof(bool), typeMapping) + { + if (array.Type.TryGetElementType(typeof(IEnumerable<>)) is null) + { + throw new ArgumentException("Array expression must be an IEnumerable", nameof(array)); + } + + Item = item; + Array = array; + OperatorType = operatorType; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBAllExpression Update(SqlExpression item, SqlExpression array) + => item != Item || array != Array + ? new GaussDBAllExpression(item, array, OperatorType, TypeMapping) + : this; + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBAllExpression).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)); + + /// + public override bool Equals(object? obj) + => obj is GaussDBAllExpression e && Equals(e); + + /// + public virtual bool Equals(GaussDBAllExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Item.Equals(other.Item) + && Array.Equals(other.Array) + && OperatorType.Equals(other.OperatorType); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Item, Array, OperatorType); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Item); + expressionPrinter + .Append(" ") + .Append( + OperatorType switch + { + PgAllOperatorType.Like => "LIKE", + PgAllOperatorType.ILike => "ILIKE", + + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") + }) + .Append(" ALL("); + expressionPrinter.Visit(Array); + expressionPrinter.Append(")"); + } + + /// + public override string ToString() + => $"{Item} {OperatorType} ALL({Array})"; +} + +/// +/// Determines the operator type for a . +/// +public enum PgAllOperatorType +{ + /// + /// Represents a GaussDB LIKE ALL operator. + /// + Like, + + /// + /// Represents a GaussDB ILIKE ALL operator. + /// + ILike, +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBAnyExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBAnyExpression.cs new file mode 100644 index 0000000000..7095866054 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBAnyExpression.cs @@ -0,0 +1,154 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents a GaussDB array ANY expression. +/// +/// +/// 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}') +/// +/// +/// See https://www.postgresql.org/docs/current/static/functions-comparisons.html +/// +public class GaussDBAnyExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + public override Type Type + => typeof(bool); + + /// + /// The value to test against the . + /// + public virtual SqlExpression Item { get; } + + /// + /// The array of values or patterns to test for the . + /// + public virtual SqlExpression Array { get; } + + /// + /// The operator. + /// + public virtual PgAnyOperatorType OperatorType { get; } + + /// + /// Constructs a . + /// + /// The operator symbol to the array expression. + /// The value to find. + /// The array to search. + /// The type mapping for the expression. + public GaussDBAnyExpression( + SqlExpression item, + SqlExpression array, + PgAnyOperatorType operatorType, + RelationalTypeMapping? typeMapping) + : base(typeof(bool), typeMapping) + { + if (array is not SqlConstantExpression { Value: null }) + { + if (array.Type.TryGetElementType(typeof(IEnumerable<>)) is null) + { + throw new ArgumentException("Array expression must be an IEnumerable", nameof(array)); + } + + if (array is SqlConstantExpression && operatorType == PgAnyOperatorType.Equal) + { + throw new ArgumentException($"Use {nameof(InExpression)} for equality against constant arrays", nameof(array)); + } + } + + Item = item; + Array = array; + OperatorType = operatorType; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBAnyExpression Update(SqlExpression item, SqlExpression array) + => item != Item || array != Array + ? new GaussDBAnyExpression(item, array, OperatorType, TypeMapping) + : this; + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBAnyExpression).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)); + + /// + public override bool Equals(object? obj) + => obj is GaussDBAnyExpression e && Equals(e); + + /// + public virtual bool Equals(GaussDBAnyExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Item.Equals(other.Item) + && Array.Equals(other.Array) + && OperatorType.Equals(other.OperatorType); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Item, Array, OperatorType); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Item); + expressionPrinter + .Append(" ") + .Append( + OperatorType switch + { + PgAnyOperatorType.Equal => "=", + PgAnyOperatorType.Like => "LIKE", + PgAnyOperatorType.ILike => "ILIKE", + + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") + }) + .Append(" ANY("); + expressionPrinter.Visit(Array); + expressionPrinter.Append(")"); + } + + /// + public override string ToString() + => $"{Item} {OperatorType} ANY({Array})"; +} + +/// +/// Determines the operator type for a . +/// +public enum PgAnyOperatorType +{ + /// + /// Represents a GaussDB = ANY operator. + /// + Equal, + + /// + /// Represents a GaussDB LIKE ANY operator. + /// + Like, + + /// + /// Represents a GaussDB ILIKE ANY operator. + /// + ILike, +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBArrayIndexExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBArrayIndexExpression.cs new file mode 100644 index 0000000000..87f92e1089 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBArrayIndexExpression.cs @@ -0,0 +1,124 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// An SQL expression that represents an indexing into a GaussDB array. +/// +/// +/// specifically disallows having an +/// of value as arrays are a GaussDB-only feature. +/// +public class GaussDBArrayIndexExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// The array being indexed. + /// + public virtual SqlExpression Array { get; } + + /// + /// The index in the array. + /// + public virtual SqlExpression Index { get; } + + /// + /// Whether the expression is nullable. + /// + public virtual bool IsNullable { get; } + + /// + /// Creates a new instance of the class. + /// + /// The array tp index into. + /// An position in the array to index into. + /// Whether the expression is nullable. + /// The of the expression. + /// The associated with the expression. + public GaussDBArrayIndexExpression( + SqlExpression array, + SqlExpression index, + bool nullable, + Type type, + RelationalTypeMapping? typeMapping) + : base(type.UnwrapNullableType(), typeMapping) + { + Check.NotNull(array, nameof(array)); + Check.NotNull(index, nameof(index)); + + if (!array.Type.TryGetElementType(out var elementType)) + { + throw new ArgumentException("Array expression must of an array type", nameof(array)); + } + + if (type.UnwrapNullableType() != elementType.UnwrapNullableType()) + { + throw new ArgumentException($"Mismatch between array type ({array.Type.Name}) and expression type ({type})"); + } + + if (index.Type != typeof(int)) + { + throw new ArgumentException("Index expression must of type int", nameof(index)); + } + + Array = array; + Index = index; + IsNullable = nullable; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBArrayIndexExpression Update(SqlExpression array, SqlExpression index) + => array == Array && index == Index + ? this + : new GaussDBArrayIndexExpression(array, index, IsNullable, Type, TypeMapping); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBArrayIndexExpression).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)); + + /// + public virtual bool Equals(GaussDBArrayIndexExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Array.Equals(other.Array) + && Index.Equals(other.Index) + && IsNullable == other.IsNullable; + + /// + public override bool Equals(object? obj) + => obj is GaussDBArrayIndexExpression e && Equals(e); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Array, Index, IsNullable); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Array); + expressionPrinter.Append("["); + expressionPrinter.Visit(Index); + expressionPrinter.Append("]"); + } + + /// + public override string ToString() + => $"{Array}[{Index}]"; +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBArraySliceExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBArraySliceExpression.cs new file mode 100644 index 0000000000..036e30e8ed --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBArraySliceExpression.cs @@ -0,0 +1,128 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// A SQL expression that represents a slicing into a GaussDB array (e.g. array[2:3]). +/// +/// +/// . +/// +public class GaussDBArraySliceExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// The array being sliced. + /// + public virtual SqlExpression Array { get; } + + /// + /// The lower bound of the slice. + /// + public virtual SqlExpression? LowerBound { get; } + + /// + /// The upper bound of the slice. + /// + public virtual SqlExpression? UpperBound { get; } + + /// + /// Whether the expression is nullable. + /// + public virtual bool IsNullable { get; } + + /// + /// Creates a new instance of the class. + /// + /// The array tp slice into. + /// The lower bound of the slice. + /// The upper bound of the slice. + /// Whether the expression is nullable. + /// The of the expression. + /// The associated with the expression. + public GaussDBArraySliceExpression( + SqlExpression array, + SqlExpression? lowerBound, + SqlExpression? upperBound, + bool nullable, + Type type, + RelationalTypeMapping? typeMapping) + : base(type.UnwrapNullableType(), typeMapping) + { + Check.NotNull(array, nameof(array)); + + if (lowerBound is null && upperBound is null) + { + throw new ArgumentException("At least one of lowerBound or upperBound must be provided"); + } + + Array = array; + LowerBound = lowerBound; + UpperBound = upperBound; + IsNullable = nullable; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The lower bound of the slice. + /// The upper bound of the slice. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBArraySliceExpression Update(SqlExpression array, SqlExpression? lowerBound, SqlExpression? upperBound) + => array == Array && lowerBound == LowerBound && upperBound == UpperBound + ? this + : new GaussDBArraySliceExpression(array, lowerBound, upperBound, IsNullable, Type, TypeMapping); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBArraySliceExpression).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( + (SqlExpression)visitor.Visit(Array), + (SqlExpression?)visitor.Visit(LowerBound), + (SqlExpression?)visitor.Visit(UpperBound)); + + /// + public virtual bool Equals(GaussDBArraySliceExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Array.Equals(other.Array) + && (LowerBound is null ? other.LowerBound is null : LowerBound.Equals(other.LowerBound)) + && (UpperBound is null ? other.UpperBound is null : UpperBound.Equals(other.UpperBound)) + && IsNullable == other.IsNullable; + + /// + public override bool Equals(object? obj) + => obj is GaussDBArraySliceExpression e && Equals(e); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Array, LowerBound, UpperBound); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Array); + expressionPrinter.Append("["); + expressionPrinter.Visit(LowerBound); + expressionPrinter.Append(":"); + expressionPrinter.Visit(UpperBound); + expressionPrinter.Append("]"); + } + + /// + public override string ToString() + => $"{Array}[{LowerBound}:{UpperBound}]"; +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBBinaryExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBBinaryExpression.cs new file mode 100644 index 0000000000..06963b1db2 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBBinaryExpression.cs @@ -0,0 +1,192 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// An expression that represents a GaussDB-specific binary operation in a SQL tree. +/// +public class GaussDBBinaryExpression : SqlExpression +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// Creates a new instance of the class. + /// + /// The operator to apply. + /// An expression which is left operand. + /// An expression which is right operand. + /// The of the expression. + /// The associated with the expression. + public GaussDBBinaryExpression( + GaussDBExpressionType operatorType, + SqlExpression left, + SqlExpression right, + Type type, + RelationalTypeMapping? typeMapping) + : base(type, typeMapping) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + OperatorType = operatorType; + Left = left; + Right = right; + } + + /// + /// The operator of this GaussDB binary operation. + /// + public virtual GaussDBExpressionType OperatorType { get; } + + /// + /// The left operand. + /// + public virtual SqlExpression Left { get; } + + /// + /// The right operand. + /// + public virtual SqlExpression Right { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + var left = (SqlExpression)visitor.Visit(Left); + var right = (SqlExpression)visitor.Visit(Right); + + return Update(left, right); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBBinaryExpression Update(SqlExpression left, SqlExpression right) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + return left != Left || right != Right + ? new GaussDBBinaryExpression(OperatorType, left, right, Type, TypeMapping) + : this; + } + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBBinaryExpression).GetConstructor( + [typeof(GaussDBExpressionType), 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) + { + Check.NotNull(expressionPrinter, nameof(expressionPrinter)); + + var requiresBrackets = RequiresBrackets(Left); + + if (requiresBrackets) + { + expressionPrinter.Append("("); + } + + expressionPrinter.Visit(Left); + + if (requiresBrackets) + { + expressionPrinter.Append(")"); + } + + expressionPrinter + .Append(" ") + .Append( + OperatorType switch + { + GaussDBExpressionType.Contains => "@>", + GaussDBExpressionType.ContainedBy => "<@", + GaussDBExpressionType.Overlaps => "&&", + + GaussDBExpressionType.NetworkContainedByOrEqual => "<<=", + GaussDBExpressionType.NetworkContainsOrEqual => ">>=", + GaussDBExpressionType.NetworkContainsOrContainedBy => "&&", + + GaussDBExpressionType.RangeIsStrictlyLeftOf => "<<", + GaussDBExpressionType.RangeIsStrictlyRightOf => ">>", + GaussDBExpressionType.RangeDoesNotExtendRightOf => "&<", + GaussDBExpressionType.RangeDoesNotExtendLeftOf => "&>", + GaussDBExpressionType.RangeIsAdjacentTo => "-|-", + GaussDBExpressionType.RangeUnion => "+", + GaussDBExpressionType.RangeIntersect => "*", + GaussDBExpressionType.RangeExcept => "-", + + GaussDBExpressionType.TextSearchMatch => "@@", + GaussDBExpressionType.TextSearchAnd => "&&", + GaussDBExpressionType.TextSearchOr => "||", + + GaussDBExpressionType.JsonExists => "?", + GaussDBExpressionType.JsonExistsAny => "?|", + GaussDBExpressionType.JsonExistsAll => "?&", + + GaussDBExpressionType.LTreeMatches + when Right.TypeMapping is { StoreType: "lquery" } or GaussDBArrayTypeMapping + { + ElementTypeMapping.StoreType: "lquery" + } + => "~", + GaussDBExpressionType.LTreeMatches when Right.TypeMapping?.StoreType == "ltxtquery" => "@", + GaussDBExpressionType.LTreeMatchesAny => "?", + GaussDBExpressionType.LTreeFirstAncestor => "?@>", + GaussDBExpressionType.LTreeFirstDescendent => "?<@", + GaussDBExpressionType.LTreeFirstMatches when Right.TypeMapping?.StoreType == "lquery" => "?~", + GaussDBExpressionType.LTreeFirstMatches when Right.TypeMapping?.StoreType == "ltxtquery" => "?@", + + GaussDBExpressionType.Distance => "<->", + + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") + }) + .Append(" "); + + requiresBrackets = RequiresBrackets(Right); + + if (requiresBrackets) + { + expressionPrinter.Append("("); + } + + expressionPrinter.Visit(Right); + + if (requiresBrackets) + { + expressionPrinter.Append(")"); + } + + static bool RequiresBrackets(SqlExpression expression) + => expression is GaussDBBinaryExpression or LikeExpression; + } + + /// + public override bool Equals(object? obj) + => obj is not null + && (ReferenceEquals(this, obj) + || obj is GaussDBBinaryExpression sqlBinaryExpression + && Equals(sqlBinaryExpression)); + + private bool Equals(GaussDBBinaryExpression sqlBinaryExpression) + => base.Equals(sqlBinaryExpression) + && OperatorType == sqlBinaryExpression.OperatorType + && Left.Equals(sqlBinaryExpression.Left) + && Right.Equals(sqlBinaryExpression.Right); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), OperatorType, Left, Right); +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBDeleteExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBDeleteExpression.cs new file mode 100644 index 0000000000..da928f5746 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBDeleteExpression.cs @@ -0,0 +1,110 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// An SQL expression that represents a GaussDB DELETE operation. +/// +public sealed class GaussDBDeleteExpression : Expression, IPrintableExpression +{ + /// + /// The tables that rows are to be deleted from. + /// + public TableExpression Table { get; } + + /// + /// Additional tables which can be referenced in the predicate. + /// + public IReadOnlyList FromItems { get; } + + /// + /// The WHERE predicate for the DELETE. + /// + public SqlExpression? Predicate { get; } + + /// + /// The list of tags applied to this . + /// + public ISet Tags { get; } + + /// + /// Creates a new instance of the class. + /// + public GaussDBDeleteExpression( + TableExpression table, + IReadOnlyList fromItems, + SqlExpression? predicate, + ISet tags) + { + (Table, FromItems, Predicate, Tags) = (table, fromItems, predicate, tags); + } + + /// + public override Type Type + => typeof(object); + + /// + public override ExpressionType NodeType + => ExpressionType.Extension; + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Predicate is null + ? this + : Update((SqlExpression?)visitor.Visit(Predicate)); + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + public GaussDBDeleteExpression Update(SqlExpression? predicate) + => predicate == Predicate + ? this + : new GaussDBDeleteExpression(Table, FromItems, predicate, Tags); + + /// + public void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.AppendLine($"DELETE FROM {Table.Name} AS {Table.Alias}"); + + if (FromItems.Count > 0) + { + var first = true; + foreach (var fromItem in FromItems) + { + if (first) + { + expressionPrinter.Append("USING "); + first = false; + } + else + { + expressionPrinter.Append(", "); + } + + expressionPrinter.Visit(fromItem); + } + } + + if (Predicate is not null) + { + expressionPrinter.Append("WHERE "); + expressionPrinter.Visit(Predicate); + } + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is GaussDBDeleteExpression pgDeleteExpression + && Equals(pgDeleteExpression)); + + private bool Equals(GaussDBDeleteExpression pgDeleteExpression) + => Table == pgDeleteExpression.Table + && FromItems.SequenceEqual(pgDeleteExpression.FromItems) + && (Predicate is null ? pgDeleteExpression.Predicate is null : Predicate.Equals(pgDeleteExpression.Predicate)); + + /// + public override int GetHashCode() + => Table.GetHashCode(); +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBFunctionExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBFunctionExpression.cs new file mode 100644 index 0000000000..8b7f4583cf --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBFunctionExpression.cs @@ -0,0 +1,326 @@ +#pragma warning disable 8632 + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents a SQL function call expression, supporting GaussDB's named parameter notation +/// (e.g. make_interval(weeks => 2) and non-comma parameter separators (e.g. position(substring in string)). +/// +public class GaussDBFunctionExpression : SqlFunctionExpression, IEquatable +{ + /// + public new virtual IReadOnlyList Arguments + => base.Arguments!; + + /// + public new virtual IReadOnlyList ArgumentsPropagateNullability + => base.ArgumentsPropagateNullability!; + + /// + /// For aggregate methods, contains whether to apply distinct. + /// + public virtual bool IsAggregateDistinct { get; } + + /// + /// For aggregate methods, contains the predicate to be applied (generated as the SQL FILTER clause). + /// + public virtual SqlExpression? AggregatePredicate { get; } + + /// + /// For aggregate methods, contains the orderings to be applied. + /// + public virtual IReadOnlyList AggregateOrderings { get; } + + /// + /// List of argument names, corresponding position-wise to arguments in . + /// Unnamed (positional) arguments must come first, so this list must contain possible nulls, followed by + /// non-nulls. + /// + public virtual IReadOnlyList ArgumentNames { get; } + + /// + /// List of non-comma separators between argument separators, in the order in which they appear between + /// the arguments. null as well as positions beyond the end of the list mean regular commas. + /// + public virtual IReadOnlyList ArgumentSeparators { get; } + + /// + /// Creates an instance of with named arguments. + /// + public static GaussDBFunctionExpression CreateWithNamedArguments( + string name, + IEnumerable arguments, + IEnumerable argumentNames, + bool nullable, + IEnumerable argumentsPropagateNullability, + bool builtIn, + Type type, + RelationalTypeMapping? typeMapping) + { + Check.NotNull(arguments, nameof(arguments)); + Check.NotNull(argumentNames, nameof(argumentNames)); + + return new GaussDBFunctionExpression( + name, arguments, argumentNames, argumentSeparators: null, + aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: [], + nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping); + } + + /// + /// Creates an instance of with argument separators. + /// + public static GaussDBFunctionExpression CreateWithArgumentSeparators( + string name, + IEnumerable arguments, + IEnumerable argumentSeparators, + bool nullable, + IEnumerable argumentsPropagateNullability, + bool builtIn, + Type type, + RelationalTypeMapping? typeMapping) + { + Check.NotNull(arguments, nameof(arguments)); + Check.NotNull(argumentSeparators, nameof(argumentSeparators)); + + return new GaussDBFunctionExpression( + name, arguments, argumentNames: null, argumentSeparators: argumentSeparators, + aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: [], + nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping); + } + + /// + /// Creates a new instance of . + /// + public GaussDBFunctionExpression( + string name, + IEnumerable arguments, + IEnumerable? argumentNames, + IEnumerable? argumentSeparators, + bool aggregateDistinct, + SqlExpression? aggregatePredicate, + IReadOnlyList aggregateOrderings, + bool nullable, + IEnumerable argumentsPropagateNullability, + Type type, + RelationalTypeMapping? typeMapping) + : base(name, arguments, nullable, argumentsPropagateNullability, type, typeMapping) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(type, nameof(type)); + + ArgumentNames = (argumentNames ?? []).ToList(); + ArgumentSeparators = (argumentSeparators ?? []).ToList(); + + if (ArgumentNames.SkipWhile(a => a is null).Contains(null)) + { + throw new ArgumentException($"{nameof(argumentNames)} must contain nulls followed by non-nulls", nameof(argumentNames)); + } + + IsAggregateDistinct = aggregateDistinct; + AggregatePredicate = aggregatePredicate; + AggregateOrderings = aggregateOrderings; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + var changed = false; + + // Note that we don't have instance functions in PG + + SqlExpression[]? visitedArguments = null; + + if (!IsNiladic) + { + for (var i = 0; i < Arguments.Count; i++) + { + var visitedArgument = (SqlExpression)visitor.Visit(Arguments[i]); + + if (visitedArgument != Arguments[i] && visitedArguments is null) + { + changed = true; + visitedArguments = new SqlExpression[Arguments.Count]; + + for (var j = 0; j < visitedArguments.Length; j++) + { + visitedArguments[j] = Arguments[j]; + } + } + + if (visitedArguments is not null) + { + visitedArguments[i] = visitedArgument; + } + } + } + + var visitedAggregatePredicate = (SqlExpression?)visitor.Visit(AggregatePredicate); + changed |= visitedAggregatePredicate != AggregatePredicate; + + OrderingExpression[]? visitedAggregateOrderings = null; + + for (var i = 0; i < AggregateOrderings.Count; i++) + { + var visitedOrdering = (OrderingExpression)visitor.Visit(AggregateOrderings[i]); + if (visitedOrdering != AggregateOrderings[i] && visitedAggregateOrderings is null) + { + changed = true; + visitedAggregateOrderings = new OrderingExpression[AggregateOrderings.Count]; + + for (var j = 0; j < visitedAggregateOrderings.Length; j++) + { + visitedAggregateOrderings[j] = AggregateOrderings[j]; + } + } + + if (visitedAggregateOrderings is not null) + { + visitedAggregateOrderings[i] = visitedOrdering; + } + } + + return changed + ? new GaussDBFunctionExpression( + Name, visitedArguments ?? Arguments, ArgumentNames, ArgumentSeparators, + IsAggregateDistinct, + visitedAggregatePredicate ?? AggregatePredicate, + visitedAggregateOrderings ?? AggregateOrderings, + IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping) + : this; + } + + /// + public override SqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + => new GaussDBFunctionExpression( + Name, + Arguments, + ArgumentNames, + ArgumentSeparators, + IsAggregateDistinct, + AggregatePredicate, + AggregateOrderings, + IsNullable, + ArgumentsPropagateNullability, Type, typeMapping ?? TypeMapping); + + /// + public override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList? arguments) + { + Check.NotNull(arguments, nameof(arguments)); + + if (instance is not null) + { + throw new ArgumentException("Must be null", nameof(instance)); + } + + return !arguments.SequenceEqual(Arguments) + ? new GaussDBFunctionExpression( + Name, arguments, ArgumentNames, ArgumentSeparators, + IsAggregateDistinct, + AggregatePredicate, + AggregateOrderings, + IsNullable, ArgumentsPropagateNullability, Type, TypeMapping) + : this; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual GaussDBFunctionExpression UpdateAggregateComponents( + SqlExpression? predicate, + IReadOnlyList orderings) + => predicate != AggregatePredicate || orderings != AggregateOrderings + ? new GaussDBFunctionExpression( + Name, Arguments, ArgumentNames, ArgumentSeparators, + IsAggregateDistinct, + predicate, + orderings, + IsNullable, ArgumentsPropagateNullability, Type, TypeMapping) + : this; + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + if (!string.IsNullOrEmpty(Schema)) + { + expressionPrinter.Append(Schema).Append(".").Append(Name); + } + else + { + Check.DebugAssert(Instance is null, "Instance is null"); + + expressionPrinter.Append(Name); + } + + if (!IsNiladic) + { + expressionPrinter.Append("("); + + if (IsAggregateDistinct) + { + expressionPrinter.Append("DISTINCT "); + } + + expressionPrinter.VisitCollection(Arguments); + + if (AggregateOrderings.Count > 0) + { + expressionPrinter.Append(" ORDER BY "); + expressionPrinter.VisitCollection(AggregateOrderings); + } + + expressionPrinter.Append(")"); + + if (AggregatePredicate is not null) + { + expressionPrinter.Append(" FILTER (WHERE "); + expressionPrinter.Visit(AggregatePredicate); + expressionPrinter.Append(")"); + } + } + } + + /// + public override bool Equals(object? obj) + => obj is GaussDBFunctionExpression pgFunction && Equals(pgFunction); + + /// + public virtual bool Equals(GaussDBFunctionExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && ArgumentNames.SequenceEqual(other.ArgumentNames) + && ArgumentSeparators.SequenceEqual(other.ArgumentSeparators) + && AggregateOrderings.SequenceEqual(other.AggregateOrderings) + && (AggregatePredicate is null && other.AggregatePredicate is null + || AggregatePredicate != null && AggregatePredicate.Equals(other.AggregatePredicate)); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + + hash.Add(base.GetHashCode()); + + foreach (var argumentName in ArgumentNames) + { + hash.Add(argumentName); + } + + foreach (var argumentSeparator in ArgumentSeparators) + { + hash.Add(argumentSeparator); + } + + foreach (var aggregateOrdering in AggregateOrderings) + { + hash.Add(aggregateOrdering); + } + + hash.Add(AggregatePredicate); + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBILikeExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBILikeExpression.cs new file mode 100644 index 0000000000..fcc12d55a7 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBILikeExpression.cs @@ -0,0 +1,109 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents a GaussDB ILIKE expression. +/// +// ReSharper disable once InconsistentNaming +public class GaussDBILikeExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// The match expression. + /// + public virtual SqlExpression Match { get; } + + /// + /// The pattern to match. + /// + public virtual SqlExpression Pattern { get; } + + /// + /// The escape character to use in . + /// + public virtual SqlExpression? EscapeChar { get; } + + /// + /// Constructs a . + /// + /// The expression to match. + /// The pattern to match. + /// The escape character to use in . + /// The associated with the expression. + /// + public GaussDBILikeExpression( + SqlExpression match, + SqlExpression pattern, + SqlExpression? escapeChar, + RelationalTypeMapping? typeMapping) + : base(typeof(bool), typeMapping) + { + Match = match; + Pattern = pattern; + EscapeChar = escapeChar; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update( + (SqlExpression)visitor.Visit(Match), + (SqlExpression)visitor.Visit(Pattern), + EscapeChar is null ? null : (SqlExpression)visitor.Visit(EscapeChar)); + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual GaussDBILikeExpression Update( + SqlExpression match, + SqlExpression pattern, + SqlExpression? escapeChar) + => match == Match && pattern == Pattern && escapeChar == EscapeChar + ? this + : new GaussDBILikeExpression(match, pattern, escapeChar, TypeMapping); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBILikeExpression).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 GaussDBILikeExpression other && Equals(other); + + /// + public virtual bool Equals(GaussDBILikeExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Equals(Match, other.Match) + && Equals(Pattern, other.Pattern) + && Equals(EscapeChar, other.EscapeChar); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Match, Pattern, EscapeChar); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Match); + expressionPrinter.Append(" ILIKE "); + expressionPrinter.Visit(Pattern); + + if (EscapeChar is not null) + { + expressionPrinter.Append(" ESCAPE "); + expressionPrinter.Visit(EscapeChar); + } + } + + /// + public override string ToString() + => $"{Match} ILIKE {Pattern}{(EscapeChar is null ? "" : $" ESCAPE {EscapeChar}")}"; +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBJsonTraversalExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBJsonTraversalExpression.cs new file mode 100644 index 0000000000..8ba3f4a898 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBJsonTraversalExpression.cs @@ -0,0 +1,125 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents a GaussDB JSON operator traversing a JSON document with a path (i.e. x#>y or x#>>y) +/// +public class GaussDBJsonTraversalExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// The match expression. + /// + public virtual SqlExpression Expression { get; } + + /// + /// The pattern to match. + /// + public virtual IReadOnlyList Path { get; } + + /// + /// Whether the text-returning operator (x#>>y) or the object-returning operator (x#>y) is used. + /// + public virtual bool ReturnsText { get; } + + /// + /// Constructs a . + /// + public GaussDBJsonTraversalExpression( + SqlExpression expression, + IReadOnlyList path, + bool returnsText, + Type type, + RelationalTypeMapping? typeMapping) + : base(type, typeMapping) + { + if (returnsText && type != typeof(string)) + { + throw new ArgumentException($"{nameof(type)} must be string", nameof(type)); + } + + Expression = expression; + Path = path; + ReturnsText = returnsText; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update( + (SqlExpression)visitor.Visit(Expression), + Path.Select(p => (SqlExpression)visitor.Visit(p)).ToArray()); + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual GaussDBJsonTraversalExpression Update(SqlExpression expression, IReadOnlyList path) + => expression == Expression && path.Count == Path.Count && path.Zip(Path, (x, y) => (x, y)).All(tup => tup.x == tup.y) + ? this + : new GaussDBJsonTraversalExpression(expression, path, ReturnsText, Type, TypeMapping); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBJsonTraversalExpression).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. + /// + public virtual GaussDBJsonTraversalExpression Append(SqlExpression pathComponent) + { + var newPath = new SqlExpression[Path.Count + 1]; + for (var i = 0; i < Path.Count(); i++) + { + newPath[i] = Path[i]; + } + + newPath[newPath.Length - 1] = pathComponent; + return new GaussDBJsonTraversalExpression(Expression, newPath, ReturnsText, Type, TypeMapping); + } + + /// + public override bool Equals(object? obj) + => Equals(obj as GaussDBJsonTraversalExpression); + + /// + public virtual bool Equals(GaussDBJsonTraversalExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Equals(Expression, other.Expression) + && Path.Count == other.Path.Count + && Path.Zip(other.Path, (x, y) => (x, y)).All(tup => tup.x == tup.y); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Expression, Path); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Expression); + expressionPrinter.Append(ReturnsText ? "#>>" : "#>"); + expressionPrinter.Append("{"); + for (var i = 0; i < Path.Count; i++) + { + expressionPrinter.Visit(Path[i]); + if (i < Path.Count - 1) + { + expressionPrinter.Append(","); + } + } + + expressionPrinter.Append("}"); + } + + /// + public override string ToString() + => $"{Expression}{(ReturnsText ? "#>>" : "#>")}{Path}"; +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBNewArrayExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBNewArrayExpression.cs new file mode 100644 index 0000000000..8509ee1191 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBNewArrayExpression.cs @@ -0,0 +1,135 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents creating a new GaussDB array. +/// +public class GaussDBNewArrayExpression : SqlExpression +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// Creates a new instance of the class. + /// + /// The values to initialize the elements of the new array. + /// The of the expression. + /// The associated with the expression. + public GaussDBNewArrayExpression( + IReadOnlyList expressions, + Type type, + RelationalTypeMapping? typeMapping) + : base(type, typeMapping) + { + Check.NotNull(expressions, nameof(expressions)); + + if (type.TryGetElementType(typeof(IEnumerable<>)) is null) + { + throw new ArgumentException($"{nameof(GaussDBNewArrayExpression)} must have an IEnumerable type"); + } + + Expressions = expressions; + } + + /// + /// The operator of this GaussDB binary operation. + /// + public virtual IReadOnlyList Expressions { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + List? newExpressions = null; + for (var i = 0; i < Expressions.Count; i++) + { + var expression = Expressions[i]; + var visitedExpression = (SqlExpression)visitor.Visit(expression); + if (visitedExpression != expression && newExpressions is null) + { + newExpressions = []; + for (var j = 0; j < i; j++) + { + newExpressions.Add(Expressions[j]); + } + } + + newExpressions?.Add(visitedExpression); + } + + return newExpressions is null + ? this + : new GaussDBNewArrayExpression(newExpressions, Type, TypeMapping); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The values to initialize the elements of the new array. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBNewArrayExpression Update(IReadOnlyList expressions) + { + Check.NotNull(expressions, nameof(expressions)); + + return expressions == Expressions + ? this + : new GaussDBNewArrayExpression(expressions, Type, TypeMapping); + } + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBNewArrayExpression).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) + { + Check.NotNull(expressionPrinter, nameof(expressionPrinter)); + + expressionPrinter.Append("ARRAY["); + + var first = true; + foreach (var expression in Expressions) + { + if (!first) + { + expressionPrinter.Append(", "); + } + + first = false; + + expressionPrinter.Visit(expression); + } + + expressionPrinter.Append("]"); + } + + /// + public override bool Equals(object? obj) + => obj is not null + && (ReferenceEquals(this, obj) + || obj is GaussDBNewArrayExpression sqlBinaryExpression + && Equals(sqlBinaryExpression)); + + private bool Equals(GaussDBNewArrayExpression pgNewArrayExpression) + => base.Equals(pgNewArrayExpression) + && Expressions.SequenceEqual(pgNewArrayExpression.Expressions); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + + hash.Add(base.GetHashCode()); + for (var i = 0; i < Expressions.Count; i++) + { + hash.Add(Expressions[i]); + } + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBRegexMatchExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBRegexMatchExpression.cs new file mode 100644 index 0000000000..670904c86e --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBRegexMatchExpression.cs @@ -0,0 +1,101 @@ +using System.Text.RegularExpressions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// Represents a GaussDB regular expression match expression. +/// +public class GaussDBRegexMatchExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + public override Type Type + => typeof(bool); + + /// + /// The match expression. + /// + public virtual SqlExpression Match { get; } + + /// + /// The pattern to match. + /// + public virtual SqlExpression Pattern { get; } + + /// + /// The options for regular expression evaluation. + /// + public virtual RegexOptions Options { get; } + + /// + /// Constructs a . + /// + /// The expression to match. + /// The pattern to match. + /// The options for regular expression evaluation. + /// The type mapping for the expression. + public GaussDBRegexMatchExpression( + SqlExpression match, + SqlExpression pattern, + RegexOptions options, + RelationalTypeMapping? typeMapping) + : base(typeof(bool), typeMapping) + { + Match = match; + Pattern = pattern; + Options = options; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update((SqlExpression)visitor.Visit(Match), (SqlExpression)visitor.Visit(Pattern)); + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual GaussDBRegexMatchExpression Update(SqlExpression match, SqlExpression pattern) + => match != Match || pattern != Pattern + ? new GaussDBRegexMatchExpression(match, pattern, Options, TypeMapping) + : this; + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBRegexMatchExpression).GetConstructor( + [typeof(SqlExpression), typeof(SqlExpression), typeof(RegexOptions), typeof(RelationalTypeMapping)])!, + Match.Quote(), + Pattern.Quote(), + Constant(Options), + RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping)); + + /// + public virtual bool Equals(GaussDBRegexMatchExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Match.Equals(other.Match) + && Pattern.Equals(other.Pattern) + && Options.Equals(other.Options); + + /// + public override bool Equals(object? other) + => other is GaussDBRegexMatchExpression otherRegexMatch && Equals(otherRegexMatch); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Match, Pattern, Options); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Match); + expressionPrinter.Append(" ~ "); + expressionPrinter.Visit(Pattern); + } + + /// + public override string ToString() + => $"{Match} ~ {Pattern}"; +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBRowValueExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBRowValueExpression.cs new file mode 100644 index 0000000000..ba650e95f8 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBRowValueExpression.cs @@ -0,0 +1,140 @@ +using System.Runtime.CompilerServices; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// An expression that represents a GaussDB-specific row value expression in a SQL tree. +/// +/// +/// See the GaussDB docs +/// for more information. +/// +public class GaussDBRowValueExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// The values of this GaussDB row value expression. + /// + public virtual IReadOnlyList Values { get; } + + /// + public GaussDBRowValueExpression( + IReadOnlyList values, + Type type, + RelationalTypeMapping? typeMapping = null) + : base(type, typeMapping) + { + Check.NotNull(values, nameof(values)); + Check.DebugAssert(type.IsAssignableTo(typeof(ITuple)), $"Type '{type}' isn't an ITuple"); + + Values = values; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + SqlExpression[]? newRowValues = null; + + for (var i = 0; i < Values.Count; i++) + { + var rowValue = Values[i]; + var visited = (SqlExpression)visitor.Visit(rowValue); + if (visited != rowValue && newRowValues is null) + { + newRowValues = new SqlExpression[Values.Count]; + for (var j = 0; j < i; j++) + { + newRowValues[j] = Values[j]; + } + } + + if (newRowValues is not null) + { + newRowValues[i] = visited; + } + } + + return newRowValues is null ? this : new GaussDBRowValueExpression(newRowValues, Type); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual GaussDBRowValueExpression Update(IReadOnlyList values) + => values.Count == Values.Count && values.Zip(Values, (x, y) => (x, y)).All(tup => tup.x == tup.y) + ? this + : new GaussDBRowValueExpression(values, Type); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBRowValueExpression).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) + { + expressionPrinter.Append("("); + + var count = Values.Count; + for (var i = 0; i < count; i++) + { + expressionPrinter.Visit(Values[i]); + + if (i < count - 1) + { + expressionPrinter.Append(", "); + } + } + + expressionPrinter.Append(")"); + } + + /// + public override bool Equals(object? obj) + => obj is GaussDBRowValueExpression other && Equals(other); + + /// + public virtual bool Equals(GaussDBRowValueExpression? other) + { + if (other is null || !base.Equals(other) || other.Values.Count != Values.Count) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + for (var i = 0; i < Values.Count; i++) + { + if (!other.Values[i].Equals(Values[i])) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var rowValue in Values) + { + hashCode.Add(rowValue); + } + + return hashCode.ToHashCode(); + } +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBTableValuedFunctionExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBTableValuedFunctionExpression.cs new file mode 100644 index 0000000000..9d6c2bb142 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBTableValuedFunctionExpression.cs @@ -0,0 +1,174 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// An expression that represents a GaussDB unnest function call in a SQL tree. +/// +/// +/// +/// This expression is just a , adding the ability to provide an explicit column name +/// for its output (SELECT * FROM unnest(array) AS f(foo)). This is necessary since when the column name isn't explicitly +/// specified, it is automatically identical to the table alias (f above); since the table alias may get uniquified by +/// EF, this would break queries. +/// +/// +/// See unnest for more +/// information and examples. +/// +/// +/// 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 GaussDBTableValuedFunctionExpression : TableValuedFunctionExpression, IEquatable +{ + /// + /// The name of the column to be projected out from the unnest call. + /// + /// + /// 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 IReadOnlyList? ColumnInfos { get; } + + /// + /// Whether to project an additional ordinality column containing the index of each element in the array. + /// + /// + /// 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 bool WithOrdinality { 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 GaussDBTableValuedFunctionExpression( + string alias, + string name, + IReadOnlyList arguments, + IReadOnlyList? columnInfos, + bool withOrdinality = true) + : base(alias, name, schema: null, builtIn: true, arguments) + { + ColumnInfos = columnInfos; + WithOrdinality = withOrdinality; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => visitor.VisitAndConvert(Arguments) is var visitedArguments && visitedArguments == Arguments + ? this + : new GaussDBTableValuedFunctionExpression(Alias, Name, visitedArguments, 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 override GaussDBTableValuedFunctionExpression Update(IReadOnlyList arguments) + => arguments.SequenceEqual(Arguments, ReferenceEqualityComparer.Instance) + ? this + : new GaussDBTableValuedFunctionExpression(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 GaussDBTableValuedFunctionExpression(Alias, Name, arguments, ColumnInfos, WithOrdinality); + } + + /// + public override GaussDBTableValuedFunctionExpression 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 GaussDBTableValuedFunctionExpression WithColumnInfos(IReadOnlyList columnInfos) + => new(Alias, Name, Arguments, columnInfos, WithOrdinality); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append(Name); + expressionPrinter.Append("("); + expressionPrinter.VisitCollection(Arguments); + expressionPrinter.Append(")"); + + if (WithOrdinality) + { + expressionPrinter.Append(" WITH ORDINALITY"); + } + + PrintAnnotations(expressionPrinter); + + expressionPrinter.Append(" AS ").Append(Alias); + + if (ColumnInfos is not null) + { + expressionPrinter.Append("("); + + var isFirst = true; + + foreach (var column in ColumnInfos) + { + if (isFirst) + { + isFirst = false; + } + else + { + expressionPrinter.Append(", "); + } + + expressionPrinter.Append(column.Name); + + if (column.TypeMapping is not null) + { + expressionPrinter.Append(" ").Append(column.TypeMapping.StoreType); + } + } + + expressionPrinter.Append(")"); + } + } + + /// + public override bool Equals(object? obj) + => ReferenceEquals(obj, this) || obj is GaussDBTableValuedFunctionExpression e && Equals(e); + + /// + public bool Equals(GaussDBTableValuedFunctionExpression? expression) + => base.Equals(expression) + && ( + expression.ColumnInfos is null && ColumnInfos is null + || expression.ColumnInfos is not null && ColumnInfos is not null && expression.ColumnInfos.SequenceEqual(ColumnInfos)) + && WithOrdinality == expression.WithOrdinality; + + /// + public override int GetHashCode() + => base.GetHashCode(); + + /// + /// Defines the name of a column coming out of a and optionally its type. + /// + public readonly record struct ColumnInfo(string Name, RelationalTypeMapping? TypeMapping = null); +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBUnknownBinaryExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBUnknownBinaryExpression.cs new file mode 100644 index 0000000000..5460d5f03f --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBUnknownBinaryExpression.cs @@ -0,0 +1,99 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// A binary expression only to be used by plugins, since new expressions can only be added (and handled) +/// within the provider itself. Allows defining the operator as a string within the expression, and has +/// default (i.e. propagating) nullability semantics. +/// All type mappings must be applied to the operands before the expression is constructed, since there's +/// no inference logic for it in . +/// +public class GaussDBUnknownBinaryExpression : SqlExpression, IEquatable +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// The left-hand expression. + /// + public virtual SqlExpression Left { get; } + + /// + /// The right-hand expression. + /// + public virtual SqlExpression Right { get; } + + /// + /// The operator. + /// + public virtual string Operator { get; } + + /// + /// Constructs a . + /// + /// The left-hand expression. + /// The right-hand expression. + /// The operator symbol acting on the expression. + /// The result type. + /// The type mapping for the expression. + /// + public GaussDBUnknownBinaryExpression( + SqlExpression left, + SqlExpression right, + string binaryOperator, + Type type, + RelationalTypeMapping? typeMapping = null) + : base(type, typeMapping) + { + Left = Check.NotNull(left, nameof(left)); + Right = Check.NotNull(right, nameof(right)); + Operator = Check.NotEmpty(binaryOperator, nameof(binaryOperator)); + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update((SqlExpression)visitor.Visit(Left), (SqlExpression)visitor.Visit(Right)); + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + public virtual GaussDBUnknownBinaryExpression Update(SqlExpression left, SqlExpression right) + => left == Left && right == Right + ? this + : new GaussDBUnknownBinaryExpression(left, right, Operator, Type, TypeMapping); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(GaussDBUnknownBinaryExpression).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(GaussDBUnknownBinaryExpression? other) + => ReferenceEquals(this, other) + || other is not null && Left.Equals(other.Left) && Right.Equals(other.Right) && Operator == other.Operator; + + /// + public override bool Equals(object? obj) + => obj is GaussDBUnknownBinaryExpression e && Equals(e); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Left, Right, Operator); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Left); + expressionPrinter.Append(Operator); + expressionPrinter.Visit(Right); + } + + /// + public override string ToString() + => $"{Left} {Operator} {Right}"; +} diff --git a/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBUnnestExpression.cs b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBUnnestExpression.cs new file mode 100644 index 0000000000..3509d0f0ec --- /dev/null +++ b/src/EFCore.GaussDB/Query/Expressions/Internal/GaussDBUnnestExpression.cs @@ -0,0 +1,114 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +/// +/// An expression that represents a GaussDB unnest function call in a SQL tree. +/// +/// +/// +/// This expression is just a , adding the ability to provide an explicit column name +/// for its output (SELECT * FROM unnest(array) AS f(foo)). This is necessary since when the column name isn't explicitly +/// specified, it is automatically identical to the table alias (f above); since the table alias may get uniquified by +/// EF, this would break queries. +/// +/// +/// See unnest for more +/// information and examples. +/// +/// +/// 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 GaussDBUnnestExpression : GaussDBTableValuedFunctionExpression +{ + /// + /// The array to be un-nested into a table. + /// + /// + /// 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 Array + => Arguments[0]; + + /// + /// The name of the column to be projected out from the unnest call. + /// + /// + /// 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 string ColumnName + => ColumnInfos![0].Name; + + /// + /// 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 GaussDBUnnestExpression(string alias, SqlExpression array, string columnName, bool withOrdinality = true) + : this(alias, array, new ColumnInfo(columnName), withOrdinality) + { + } + + private GaussDBUnnestExpression(string alias, SqlExpression array, ColumnInfo? columnInfo, bool withOrdinality = true) + : base(alias, "unnest", [array], columnInfo is null ? null : [columnInfo.Value], withOrdinality) + { + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => visitor.Visit(Array) is var visitedArray && visitedArray == Array + ? this + : new GaussDBUnnestExpression(Alias, (SqlExpression)visitedArray, ColumnName, 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 override GaussDBUnnestExpression Update(IReadOnlyList arguments) + => arguments is [var singleArgument] + ? Update(singleArgument) + : throw new ArgumentException(); + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual GaussDBUnnestExpression Update(SqlExpression array) + => array == Array + ? this + : new GaussDBUnnestExpression(Alias, array, ColumnName, WithOrdinality); + + /// + public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloningExpressionVisitor) + => new GaussDBUnnestExpression(alias!, (SqlExpression)cloningExpressionVisitor.Visit(Array), ColumnName, WithOrdinality); + + /// + public override GaussDBUnnestExpression WithAlias(string newAlias) + => new(newAlias, Array, ColumnName, WithOrdinality); + + /// + public override GaussDBUnnestExpression WithColumnInfos(IReadOnlyList columnInfos) + => new( + Alias, + Array, + columnInfos switch + { + [] => null, + [var columnInfo] => columnInfo, + _ => throw new ArgumentException() + }, + WithOrdinality); +} diff --git a/src/EFCore.GaussDB/Query/GaussDBExpressionFactory.cs b/src/EFCore.GaussDB/Query/GaussDBExpressionFactory.cs new file mode 100644 index 0000000000..196d37c2c1 --- /dev/null +++ b/src/EFCore.GaussDB/Query/GaussDBExpressionFactory.cs @@ -0,0 +1,1057 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query; + +/// +public class GaussDBExpressionFactory : SqlExpressionFactory +{ + private readonly GaussDBTypeMappingSource _typeMappingSource; + private readonly RelationalTypeMapping _boolTypeMapping; + + private static Type? _nodaTimeDurationType; + private static Type? _nodaTimePeriodType; + + /// + /// Creates a new instance of the class. + /// + /// Parameter object containing dependencies for this class. + public GaussDBExpressionFactory(SqlExpressionFactoryDependencies dependencies) + : base(dependencies) + { + _typeMappingSource = (GaussDBTypeMappingSource)dependencies.TypeMappingSource; + _boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!; + } + + #region Expression factory methods + + /// + /// Creates a new , corresponding to the GaussDB-specific ~ operator. + /// + public virtual GaussDBRegexMatchExpression RegexMatch( + SqlExpression match, + SqlExpression pattern, + RegexOptions options) + => (GaussDBRegexMatchExpression)ApplyDefaultTypeMapping(new GaussDBRegexMatchExpression(match, pattern, options, null)); + + /// + /// Creates a new , corresponding to the GaussDB-specific = ANY operator. + /// + public virtual GaussDBAnyExpression Any( + SqlExpression item, + SqlExpression array, + PgAnyOperatorType operatorType) + => (GaussDBAnyExpression)ApplyDefaultTypeMapping(new GaussDBAnyExpression(item, array, operatorType, null)); + + /// + /// Creates a new , corresponding to the GaussDB-specific LIKE ALL operator. + /// + public virtual GaussDBAllExpression All( + SqlExpression item, + SqlExpression array, + PgAllOperatorType operatorType) + => (GaussDBAllExpression)ApplyDefaultTypeMapping(new GaussDBAllExpression(item, array, operatorType, null)); + + /// + /// Creates a new , corresponding to the GaussDB-specific array subscripting operator. + /// + public virtual GaussDBArrayIndexExpression ArrayIndex( + SqlExpression array, + SqlExpression index, + bool nullable, + RelationalTypeMapping? typeMapping = null) + { + if (!array.Type.TryGetElementType(out var elementType)) + { + throw new ArgumentException("Array expression must be of an array or List<> type", nameof(array)); + } + + return (GaussDBArrayIndexExpression)ApplyTypeMapping( + new GaussDBArrayIndexExpression(array, index, nullable, elementType, typeMapping: null), + typeMapping); + } + + /// + /// Creates a new , corresponding to the GaussDB-specific array subscripting operator. + /// + public virtual GaussDBArraySliceExpression ArraySlice( + SqlExpression array, + SqlExpression? lowerBound, + SqlExpression? upperBound, + bool nullable, + RelationalTypeMapping? typeMapping = null) + => (GaussDBArraySliceExpression)ApplyTypeMapping( + new GaussDBArraySliceExpression(array, lowerBound, upperBound, nullable, array.Type, typeMapping: null), + typeMapping); + + /// + /// Creates a new , for converting a timestamp to UTC. + /// + public virtual AtTimeZoneExpression AtUtc( + SqlExpression timestamp, + RelationalTypeMapping? typeMapping = null) + => AtTimeZone(timestamp, Constant("UTC"), timestamp.Type); + + /// + /// Creates a new , for converting a timestamp to another time zone. + /// + public virtual AtTimeZoneExpression AtTimeZone( + SqlExpression timestamp, + SqlExpression timeZone, + Type type, + RelationalTypeMapping? typeMapping = null) + { + if (typeMapping is null) + { + // GaussDB AT TIME ZONE flips the given type from timestamptz to timestamp and vice versa + // See https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-ZONECONVERT + typeMapping = timestamp.TypeMapping ?? _typeMappingSource.FindMapping(timestamp.Type, Dependencies.Model)!; + var storeType = typeMapping.StoreType; + + typeMapping = storeType.StartsWith("timestamp with time zone", StringComparison.Ordinal) + || storeType.StartsWith("timestamptz", StringComparison.Ordinal) + ? _typeMappingSource.FindMapping("timestamp without time zone")! + : storeType.StartsWith("timestamp without time zone", StringComparison.Ordinal) + || storeType.StartsWith("timestamp", StringComparison.Ordinal) + ? _typeMappingSource.FindMapping("timestamp with time zone")! + : throw new ArgumentException( + $"timestamp argument to AtTimeZone had unknown store type {storeType}", nameof(timestamp)); + } + + return new AtTimeZoneExpression( + ApplyDefaultTypeMapping(timestamp), + ApplyDefaultTypeMapping(timeZone), + type, + typeMapping); + } + + /// + /// Creates a new , for performing a GaussDB-specific case-insensitive string match + /// (ILIKE). + /// + public virtual GaussDBILikeExpression ILike( + SqlExpression match, + SqlExpression pattern, + SqlExpression? escapeChar = null) + => (GaussDBILikeExpression)ApplyDefaultTypeMapping(new GaussDBILikeExpression(match, pattern, escapeChar, null)); + + /// + /// Creates a new , for traversing inside a JSON document. + /// + public virtual GaussDBJsonTraversalExpression JsonTraversal( + SqlExpression expression, + bool returnsText, + Type type, + RelationalTypeMapping? typeMapping = null) + => JsonTraversal(expression, [], returnsText, type, typeMapping); + + /// + /// Creates a new , for traversing inside a JSON document. + /// + public virtual GaussDBJsonTraversalExpression JsonTraversal( + SqlExpression expression, + IEnumerable path, + bool returnsText, + Type type, + RelationalTypeMapping? typeMapping = null) + => new( + ApplyDefaultTypeMapping(expression), + path.Select(ApplyDefaultTypeMapping).ToArray()!, + returnsText, + type, + typeMapping); + + /// + /// Constructs either a , or, if all provided expressions are constants, a single + /// for the entire array. + /// + public virtual SqlExpression NewArrayOrConstant( + IReadOnlyList elements, + Type type, + RelationalTypeMapping? typeMapping = null) + { + var elementType = type.TryGetElementType(typeof(IEnumerable<>)); + if (elementType is null) + { + throw new ArgumentException($"{type.Name} isn't an IEnumerable", nameof(type)); + } + + var newArrayExpression = NewArray(elements, type, typeMapping); + + if (newArrayExpression.Expressions.Any(e => e is not SqlConstantExpression)) + { + return newArrayExpression; + } + + // All elements are constants; extract their values and return an SqlConstantExpression over the array/list + if (type.IsGenericList()) + { + var list = (IList)Activator.CreateInstance(type, elements.Count)!; + var addMethod = type.GetMethod("Add")!; + for (var i = 0; i < elements.Count; i++) + { + addMethod.Invoke(list, [((SqlConstantExpression)newArrayExpression.Expressions[i]).Value]); + } + + return Constant(list, newArrayExpression.TypeMapping); + } + + // We support any arbitrary IEnumerable as the expression type, but only support arrays and Lists as *concrete* types. + // So unless the type was a List (handled above), we return an array constant here. + var array = Array.CreateInstance(elementType, elements.Count); + for (var i = 0; i < elements.Count; i++) + { + array.SetValue(((SqlConstantExpression)newArrayExpression.Expressions[i]).Value, i); + } + + return Constant(array, newArrayExpression.TypeMapping); + } + + /// + /// Creates a new , for creating a new GaussDB array. + /// + public virtual GaussDBNewArrayExpression NewArray( + IReadOnlyList expressions, + Type type, + RelationalTypeMapping? typeMapping = null) + => (GaussDBNewArrayExpression)ApplyTypeMapping(new GaussDBNewArrayExpression(expressions, type, typeMapping), typeMapping); + + /// + public override SqlExpression? MakeBinary( + ExpressionType operatorType, + SqlExpression left, + SqlExpression right, + RelationalTypeMapping? typeMapping, + SqlExpression? existingExpr = null) + { + switch (operatorType) + { + case ExpressionType.Subtract + when left.Type == typeof(DateTime) && right.Type == typeof(DateTime) + || left.Type == typeof(DateTimeOffset) && right.Type == typeof(DateTimeOffset) + || left.Type == typeof(TimeOnly) && right.Type == typeof(TimeOnly): + { + return (SqlBinaryExpression)ApplyTypeMapping( + new SqlBinaryExpression(ExpressionType.Subtract, left, right, typeof(TimeSpan), null), typeMapping); + } + + case ExpressionType.Subtract + when left.Type.FullName == "NodaTime.Instant" && right.Type.FullName == "NodaTime.Instant" + || left.Type.FullName == "NodaTime.ZonedDateTime" && right.Type.FullName == "NodaTime.ZonedDateTime": + { + _nodaTimeDurationType ??= left.Type.Assembly.GetType("NodaTime.Duration"); + return (SqlBinaryExpression)ApplyTypeMapping( + new SqlBinaryExpression(ExpressionType.Subtract, left, right, _nodaTimeDurationType!, null), typeMapping); + } + + case ExpressionType.Subtract + when left.Type.FullName == "NodaTime.LocalDateTime" && right.Type.FullName == "NodaTime.LocalDateTime" + || left.Type.FullName == "NodaTime.LocalTime" && right.Type.FullName == "NodaTime.LocalTime": + { + _nodaTimePeriodType ??= left.Type.Assembly.GetType("NodaTime.Period"); + return (SqlBinaryExpression)ApplyTypeMapping( + new SqlBinaryExpression(ExpressionType.Subtract, left, right, _nodaTimePeriodType!, null), typeMapping); + } + + case ExpressionType.Subtract + when left.Type.FullName == "NodaTime.LocalDate" && right.Type.FullName == "NodaTime.LocalDate": + { + return (SqlBinaryExpression)ApplyTypeMapping( + new SqlBinaryExpression(ExpressionType.Subtract, left, right, typeof(int), null), typeMapping); + } + } + + return base.MakeBinary(operatorType, left, right, typeMapping, existingExpr); + } + + /// + /// Creates a new with the given arguments. + /// + /// An representing SQL unary operator. + /// The left operand of binary operation. + /// The right operand of binary operation. + /// A type mapping to be assigned to the created expression. + /// A with the given arguments. + public virtual SqlExpression MakePostgresBinary( + GaussDBExpressionType operatorType, + SqlExpression left, + SqlExpression right, + RelationalTypeMapping? typeMapping = null) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + var returnType = left.Type; + switch (operatorType) + { + case GaussDBExpressionType.Contains: + case GaussDBExpressionType.ContainedBy: + case GaussDBExpressionType.Overlaps: + case GaussDBExpressionType.NetworkContainedByOrEqual: + case GaussDBExpressionType.NetworkContainsOrEqual: + case GaussDBExpressionType.NetworkContainsOrContainedBy: + case GaussDBExpressionType.RangeIsStrictlyLeftOf: + case GaussDBExpressionType.RangeIsStrictlyRightOf: + case GaussDBExpressionType.RangeDoesNotExtendRightOf: + case GaussDBExpressionType.RangeDoesNotExtendLeftOf: + case GaussDBExpressionType.RangeIsAdjacentTo: + case GaussDBExpressionType.TextSearchMatch: + case GaussDBExpressionType.JsonExists: + case GaussDBExpressionType.JsonExistsAny: + case GaussDBExpressionType.JsonExistsAll: + returnType = typeof(bool); + break; + + case GaussDBExpressionType.Distance: + returnType = typeof(double); + break; + } + + return (GaussDBBinaryExpression)ApplyTypeMapping( + new GaussDBBinaryExpression(operatorType, left, right, returnType, null), typeMapping); + } + + /// + /// Creates a new , for checking whether one value contains another. + /// + public virtual SqlExpression Contains(SqlExpression left, SqlExpression right) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + return MakePostgresBinary(GaussDBExpressionType.Contains, left, right); + } + + /// + /// Creates a new , for checking whether one value is contained by another. + /// + public virtual SqlExpression ContainedBy(SqlExpression left, SqlExpression right) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + return MakePostgresBinary(GaussDBExpressionType.ContainedBy, left, right); + } + + /// + /// Creates a new , for checking whether one value overlaps with another. + /// + public virtual SqlExpression Overlaps(SqlExpression left, SqlExpression right) + { + Check.NotNull(left, nameof(left)); + Check.NotNull(right, nameof(right)); + + return MakePostgresBinary(GaussDBExpressionType.Overlaps, left, right); + } + + /// + /// Creates a new for a GaussDB aggregate function call.. + /// + public virtual GaussDBFunctionExpression AggregateFunction( + string name, + IEnumerable arguments, + EnumerableExpression aggregateEnumerableExpression, + bool nullable, + IEnumerable argumentsPropagateNullability, + Type returnType, + RelationalTypeMapping? typeMapping = null) + { + var typeMappedArguments = new List(); + + foreach (var argument in arguments) + { + typeMappedArguments.Add(ApplyDefaultTypeMapping(argument)); + } + + return new GaussDBFunctionExpression( + name, + typeMappedArguments, + argumentNames: null, + argumentSeparators: null, + aggregateEnumerableExpression.IsDistinct, + aggregateEnumerableExpression.Predicate, + aggregateEnumerableExpression.Orderings, + nullable: nullable, + argumentsPropagateNullability: argumentsPropagateNullability, type: returnType, typeMapping: typeMapping); + } + + #endregion Expression factory methods + + /// + [return: NotNullIfNotNull("sqlExpression")] + public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) + { + if (sqlExpression is not null && sqlExpression.TypeMapping is null) + { + sqlExpression = sqlExpression switch + { + SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping), + + // GaussDB-specific expression types + GaussDBAnyExpression e => ApplyTypeMappingOnAny(e), + GaussDBAllExpression e => ApplyTypeMappingOnAll(e), + GaussDBArrayIndexExpression e => ApplyTypeMappingOnArrayIndex(e, typeMapping), + GaussDBArraySliceExpression e => ApplyTypeMappingOnArraySlice(e, typeMapping), + GaussDBBinaryExpression e => ApplyTypeMappingOnPostgresBinary(e, typeMapping), + GaussDBFunctionExpression e => e.ApplyTypeMapping(typeMapping), + GaussDBILikeExpression e => ApplyTypeMappingOnILike(e), + GaussDBNewArrayExpression e => ApplyTypeMappingOnNewArray(e, typeMapping), + GaussDBRegexMatchExpression e => ApplyTypeMappingOnRegexMatch(e), + GaussDBRowValueExpression e => ApplyTypeMappingOnRowValue(e, typeMapping), + + _ => base.ApplyTypeMapping(sqlExpression, typeMapping) + }; + } + + if (!GaussDBTypeMappingSource.LegacyTimestampBehavior + && (typeMapping is GaussDBTimestampTypeMapping && sqlExpression?.TypeMapping is GaussDBTimestampTzTypeMapping + || typeMapping is GaussDBTimestampTzTypeMapping && sqlExpression?.TypeMapping is GaussDBTimestampTypeMapping)) + { + throw new NotSupportedException( + "Cannot apply binary operation on types 'timestamp with time zone' and 'timestamp without time zone', convert one of the operands first."); + } + + return sqlExpression; + } + + private SqlBinaryExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression binary, RelationalTypeMapping? typeMapping) + { + var (left, right) = (binary.Left, binary.Right); + + // The default SqlExpressionFactory behavior is to assume that the two added operands have the same type, + // and so to infer one side's mapping from the other if needed. Here we take care of some heterogeneous + // operand cases where this doesn't work: + // * Period + Period (???) + + switch (binary.OperatorType) + { + // DateTime + TimeSpan => DateTime + // DateTimeOffset + TimeSpan => DateTimeOffset + // TimeOnly + TimeSpan => TimeOnly + case ExpressionType.Add or ExpressionType.Subtract + when right.Type == typeof(TimeSpan) + && (left.Type == typeof(DateTime) || left.Type == typeof(DateTimeOffset) || left.Type == typeof(TimeOnly)) + || right.Type == typeof(int) && left.Type == typeof(DateOnly) + || right.Type.FullName == "NodaTime.Period" + && left.Type.FullName is "NodaTime.LocalDateTime" or "NodaTime.LocalDate" or "NodaTime.LocalTime" + || right.Type.FullName == "NodaTime.Duration" + && left.Type.FullName is "NodaTime.Instant" or "NodaTime.ZonedDateTime": + { + var newLeft = ApplyTypeMapping(left, typeMapping); + var newRight = ApplyDefaultTypeMapping(right); + return new SqlBinaryExpression(binary.OperatorType, newLeft, newRight, binary.Type, newLeft.TypeMapping); + } + + // DateTime - DateTime => TimeSpan + // DateTimeOffset - DateTimeOffset => TimeSpan + // DateOnly - DateOnly => TimeSpan + // TimeOnly - TimeOnly => TimeSpan + // Instant - Instant => Duration + // LocalDateTime - LocalDateTime => int (days) + case ExpressionType.Subtract + when left.Type == typeof(DateTime) && right.Type == typeof(DateTime) + || left.Type == typeof(DateTimeOffset) && right.Type == typeof(DateTimeOffset) + || left.Type == typeof(DateOnly) && right.Type == typeof(DateOnly) + || left.Type == typeof(TimeOnly) && right.Type == typeof(TimeOnly) + || left.Type.FullName == "NodaTime.Instant" && right.Type.FullName == "NodaTime.Instant" + || left.Type.FullName == "NodaTime.LocalDateTime" && right.Type.FullName == "NodaTime.LocalDateTime" + || left.Type.FullName == "NodaTime.ZonedDateTime" && right.Type.FullName == "NodaTime.ZonedDateTime" + || left.Type.FullName == "NodaTime.LocalDate" && right.Type.FullName == "NodaTime.LocalDate" + || left.Type.FullName == "NodaTime.LocalTime" && right.Type.FullName == "NodaTime.LocalTime": + { + var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right); + + return new SqlBinaryExpression( + ExpressionType.Subtract, + ApplyTypeMapping(left, inferredTypeMapping), + ApplyTypeMapping(right, inferredTypeMapping), + 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. + if (IsComparison(binary.OperatorType) + && TryGetRowValueValues(binary.Left, out var leftValues) + && TryGetRowValueValues(binary.Right, out var rightValues)) + { + if (leftValues.Count != rightValues.Count) + { + throw new ArgumentException(GaussDBStrings.RowValueComparisonRequiresTuplesOfSameLength); + } + + var count = leftValues.Count; + var updatedLeftValues = new SqlExpression[count]; + var updatedRightValues = new SqlExpression[count]; + + for (var i = 0; i < count; i++) + { + var updatedElementBinaryExpression = MakeBinary(binary.OperatorType, leftValues[i], rightValues[i], typeMapping: null)!; + + 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 + // SqlConstantExpression. This is because each value in the row value needs to have its type mapping. + binary = new SqlBinaryExpression( + binary.OperatorType, + new GaussDBRowValueExpression(updatedLeftValues, binary.Left.Type), + new GaussDBRowValueExpression(updatedRightValues, binary.Right.Type), + binary.Type, + binary.TypeMapping); + } + + return (SqlBinaryExpression)base.ApplyTypeMapping(binary, typeMapping); + + static bool IsComparison(ExpressionType expressionType) + { + switch (expressionType) + { + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.GreaterThan: + case ExpressionType.LessThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LessThanOrEqual: + return true; + default: + return false; + } + } + + bool TryGetRowValueValues(SqlExpression e, [NotNullWhen(true)] out IReadOnlyList? values) + { + switch (e) + { + case GaussDBRowValueExpression rowValueExpression: + values = rowValueExpression.Values; + return true; + + case SqlConstantExpression { Value : ITuple constantTuple }: + var v = new SqlExpression[constantTuple.Length]; + + for (var i = 0; i < v.Length; i++) + { + v[i] = Constant(constantTuple[i], typeof(object)); + } + + values = v; + return true; + + default: + values = null; + return false; + } + } + } + + private SqlExpression ApplyTypeMappingOnRegexMatch(GaussDBRegexMatchExpression pgRegexMatchExpression) + { + var inferredTypeMapping = ExpressionExtensions.InferTypeMapping( + pgRegexMatchExpression.Match, pgRegexMatchExpression.Pattern) + ?? _typeMappingSource.FindMapping(pgRegexMatchExpression.Match.Type, Dependencies.Model); + + return new GaussDBRegexMatchExpression( + ApplyTypeMapping(pgRegexMatchExpression.Match, inferredTypeMapping), + ApplyTypeMapping(pgRegexMatchExpression.Pattern, inferredTypeMapping), + pgRegexMatchExpression.Options, + _boolTypeMapping); + } + + private SqlExpression ApplyTypeMappingOnRowValue( + GaussDBRowValueExpression pgRowValueExpression, + RelationalTypeMapping? typeMapping) + { + // If the row value is in a binary expression (e.g. a comparison, (a, b) > (5, 6)), we have special type inference code + // to infer from the other row value in ApplyTypeMappingOnSqlBinary. + // If we're here, that means that no such inference can happen, and we just use the default type mappings. + var updatedValues = new SqlExpression[pgRowValueExpression.Values.Count]; + + for (var i = 0; i < updatedValues.Length; i++) + { + updatedValues[i] = ApplyDefaultTypeMapping(pgRowValueExpression.Values[i]); + } + + return new GaussDBRowValueExpression(updatedValues, pgRowValueExpression.Type, typeMapping); + } + + private SqlExpression ApplyTypeMappingOnAny(GaussDBAnyExpression pgAnyExpression) + { + var (item, array) = ApplyTypeMappingsOnItemAndArray(pgAnyExpression.Item, pgAnyExpression.Array); + return new GaussDBAnyExpression(item, array, pgAnyExpression.OperatorType, _boolTypeMapping); + } + + private SqlExpression ApplyTypeMappingOnAll(GaussDBAllExpression pgAllExpression) + { + var (item, array) = ApplyTypeMappingsOnItemAndArray(pgAllExpression.Item, pgAllExpression.Array); + return new GaussDBAllExpression(item, array, pgAllExpression.OperatorType, _boolTypeMapping); + } + + internal (SqlExpression, SqlExpression) ApplyTypeMappingsOnItemAndArray(SqlExpression itemExpression, SqlExpression arrayExpression) + { + // Attempt type inference either from the operand to the array or the other way around + var arrayMapping = arrayExpression.TypeMapping; + + var itemMapping = + itemExpression.TypeMapping + // Unwrap convert-to-object nodes - these get added for object[].Contains(x) + ?? (itemExpression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } unary && unary.Type == typeof(object) + ? unary.Operand.TypeMapping + : null) + // If we couldn't find a type mapping on the item, try inferring it from the array + ?? (RelationalTypeMapping?)arrayMapping?.ElementTypeMapping + ?? _typeMappingSource.FindMapping(itemExpression.Type, Dependencies.Model); + + if (itemMapping is null) + { + throw new InvalidOperationException("Couldn't find element type mapping when applying item/array mappings"); + } + + // If the array's type mapping isn't provided (parameter/constant), attempt to infer it from the item. + if (arrayMapping is null) + { + if (itemMapping.Converter is not null) + { + // If the item mapping has a value converter, construct an array mapping directly over it - this will build the + // corresponding array type converter. + arrayMapping = _typeMappingSource.FindMapping(arrayExpression.Type, Dependencies.Model, itemMapping); + } + else + { + // No value converter on the item mapping - just try to look up an array mapping based on the item type. + // Special-case arrays of objects, not taking the array CLR type into account in the lookup (it would never succeed). + // Note that we provide both the array CLR type *and* an array store type constructed from the element's store type. + // If we use only the array CLR type, byte[] will yield bytea which we don't want. + arrayMapping = arrayExpression.Type.TryGetSequenceType() == typeof(object) + ? _typeMappingSource.FindMapping(itemMapping.StoreType + "[]") + : _typeMappingSource.FindMapping(arrayExpression.Type, itemMapping.StoreType + "[]"); + } + + if (arrayMapping is null) + { + throw new InvalidOperationException("Couldn't find array type mapping when applying item/array mappings"); + } + } + + return (ApplyTypeMapping(itemExpression, itemMapping), ApplyTypeMapping(arrayExpression, arrayMapping)); + } + + private SqlExpression ApplyTypeMappingOnArrayIndex( + GaussDBArrayIndexExpression pgArrayIndexExpression, + RelationalTypeMapping? typeMapping) + { + // 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.ClrType, typeMapping), pgArrayIndexExpression.Array) + : (null, ApplyDefaultTypeMapping(pgArrayIndexExpression.Array)); + + return new GaussDBArrayIndexExpression( + array, + ApplyDefaultTypeMapping(pgArrayIndexExpression.Index), + pgArrayIndexExpression.IsNullable, + pgArrayIndexExpression.Type, + // If the array has a type mapping (i.e. column), prefer that just like we prefer column mappings in general + pgArrayIndexExpression.Array.TypeMapping is GaussDBArrayTypeMapping arrayMapping + ? arrayMapping.ElementTypeMapping + : typeMapping + ?? _typeMappingSource.FindMapping(pgArrayIndexExpression.Type, Dependencies.Model)); + } + + private SqlExpression ApplyTypeMappingOnArraySlice( + GaussDBArraySliceExpression slice, + RelationalTypeMapping? typeMapping) + { + // If the slice operand has a type mapping, that bubbles up (slice is just a view over that). Otherwise apply the external type + // mapping down. The bounds are always ints and don't participate in any inference. + + // 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 = ApplyTypeMapping(slice.Array, typeMapping); + + return new GaussDBArraySliceExpression( + array, + slice.LowerBound is null ? null : ApplyDefaultTypeMapping(slice.LowerBound), + slice.UpperBound is null ? null : ApplyDefaultTypeMapping(slice.UpperBound), + slice.IsNullable, + slice.Type, + array.TypeMapping); + } + + private SqlExpression ApplyTypeMappingOnILike(GaussDBILikeExpression ilikeExpression) + { + var inferredTypeMapping = (ilikeExpression.EscapeChar is null + ? ExpressionExtensions.InferTypeMapping( + ilikeExpression.Match, ilikeExpression.Pattern) + : ExpressionExtensions.InferTypeMapping( + ilikeExpression.Match, ilikeExpression.Pattern, + ilikeExpression.EscapeChar)) + ?? _typeMappingSource.FindMapping(ilikeExpression.Match.Type, Dependencies.Model); + + return new GaussDBILikeExpression( + ApplyTypeMapping(ilikeExpression.Match, inferredTypeMapping), + ApplyTypeMapping(ilikeExpression.Pattern, inferredTypeMapping), + ApplyTypeMapping(ilikeExpression.EscapeChar, inferredTypeMapping), + _boolTypeMapping); + } + + private SqlExpression ApplyTypeMappingOnPostgresBinary( + GaussDBBinaryExpression pgBinaryExpression, + RelationalTypeMapping? typeMapping) + { + var (left, right) = (pgBinaryExpression.Left, pgBinaryExpression.Right); + + Type resultType; + RelationalTypeMapping? resultTypeMapping = null; + RelationalTypeMapping? inferredTypeMapping; + var operatorType = pgBinaryExpression.OperatorType; + switch (operatorType) + { + case GaussDBExpressionType.Overlaps: + case GaussDBExpressionType.Contains: + case GaussDBExpressionType.ContainedBy: + case GaussDBExpressionType.RangeIsStrictlyLeftOf: + case GaussDBExpressionType.RangeIsStrictlyRightOf: + case GaussDBExpressionType.RangeDoesNotExtendRightOf: + case GaussDBExpressionType.RangeDoesNotExtendLeftOf: + case GaussDBExpressionType.RangeIsAdjacentTo: + { + resultType = typeof(bool); + resultTypeMapping = _boolTypeMapping; + + // Simple case: we have the same CLR type on both sides, or we have an array on either side + // (e.g. overlap/intersect between two arrays); note that different CLR types may be mapped to arrays on the two sides + // (e.g. int[] and List) + if (left.Type == right.Type + || left.TypeMapping is GaussDBArrayTypeMapping + || right.TypeMapping is GaussDBArrayTypeMapping) + { + inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right); + break; + } + + // Multirange and range, cidr and ip - cases of different types where one contains the other. + // We need fancier type mapping inference. + SqlExpression newLeft, newRight; + + if (operatorType == GaussDBExpressionType.ContainedBy) + { + (newRight, newLeft) = InferContainmentMappings(right, left); + } + else + { + (newLeft, newRight) = InferContainmentMappings(left, right); + } + + return new GaussDBBinaryExpression(operatorType, newLeft, newRight, resultType, resultTypeMapping); + } + + case GaussDBExpressionType.NetworkContainedByOrEqual: + case GaussDBExpressionType.NetworkContainsOrEqual: + case GaussDBExpressionType.NetworkContainsOrContainedBy: + case GaussDBExpressionType.TextSearchMatch: + case GaussDBExpressionType.JsonExists: + case GaussDBExpressionType.JsonExistsAny: + case GaussDBExpressionType.JsonExistsAll: + { + // TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are + // based on operator type? + return new GaussDBBinaryExpression( + operatorType, + ApplyDefaultTypeMapping(left), + ApplyDefaultTypeMapping(right), + typeof(bool), + _boolTypeMapping); + } + + case GaussDBExpressionType.RangeUnion: + case GaussDBExpressionType.RangeIntersect: + case GaussDBExpressionType.RangeExcept: + case GaussDBExpressionType.TextSearchAnd: + case GaussDBExpressionType.TextSearchOr: + { + inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right); + resultType = inferredTypeMapping?.ClrType ?? left.Type; + resultTypeMapping = inferredTypeMapping; + break; + } + + case GaussDBExpressionType.Distance: + { + inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right); + + resultType = inferredTypeMapping?.StoreTypeNameBase switch + { + "geometry" or "geography" => typeof(double), + + "date" => typeof(int), + + "interval" when left.Type.FullName is "NodaTime.Period" or "NodaTime.Duration" + => _nodaTimePeriodType ??= left.Type.Assembly.GetType("NodaTime.Period")!, + "interval" => typeof(TimeSpan), + + "timestamp" or "timestamptz" or "timestamp with time zone" or "timestamp without time zone" + when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "NodaTime.ZonedDateTime" + => _nodaTimePeriodType ??= left.Type.Assembly.GetType("NodaTime.Period")!, + "timestamp" or "timestamptz" or "timestamp with time zone" or "timestamp without time zone" + => typeof(TimeSpan), + + null => throw new InvalidOperationException("No inferred type mapping for distance operator"), + _ => throw new InvalidOperationException( + $"GaussDB type '{inferredTypeMapping.StoreTypeNameBase}' isn't supported with the distance operator") + }; + break; + } + + default: + throw new InvalidOperationException( + $"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}"); + } + + return new GaussDBBinaryExpression( + operatorType, + ApplyTypeMapping(left, inferredTypeMapping), + ApplyTypeMapping(right, inferredTypeMapping), + resultType, + resultTypeMapping ?? _typeMappingSource.FindMapping(resultType)); + + (SqlExpression, SqlExpression) InferContainmentMappings(SqlExpression container, SqlExpression containee) + { + Debug.Assert( + container.Type != containee.Type, + "This method isn't meant for identical types, where type mapping inference is much simpler"); + + // Attempt type inference either from the container or from the containee + var containerMapping = container.TypeMapping; + var containeeMapping = containee.TypeMapping; + + if (containeeMapping is null) + { + // If we couldn't find a type mapping on the containee, try inferring it from the container + containeeMapping = containerMapping switch + { + GaussDBRangeTypeMapping rangeTypeMapping => rangeTypeMapping.SubtypeMapping, + GaussDBMultirangeTypeMapping multirangeTypeMapping + => containee.Type.IsGenericType && containee.Type.GetGenericTypeDefinition() == typeof(GaussDBRange<>) + ? multirangeTypeMapping.RangeMapping + : multirangeTypeMapping.SubtypeMapping, + _ => null + }; + + // Apply the inferred mapping to the containee, or fall back to the default type mapping + if (containeeMapping is not null) + { + containee = ApplyTypeMapping(containee, containeeMapping); + } + else + { + containee = ApplyDefaultTypeMapping(containee); + containeeMapping = containee.TypeMapping; + + if (containeeMapping is null) + { + throw new InvalidOperationException( + "Couldn't find containee type mapping when applying container/containee mappings"); + } + } + } + + // If the container's type mapping isn't provided (parameter/constant), attempt to infer it from the item. + if (containerMapping is null) + { + // TODO: FindContainerMapping currently works for range/multirange only, may want to extend it to other types + // (e.g. IP address containment) + containerMapping = _typeMappingSource.FindContainerMapping(container.Type, containeeMapping, Dependencies.Model); + + // Apply the inferred mapping to the container, or fall back to the default type mapping + if (containerMapping is not null) + { + container = ApplyTypeMapping(container, containerMapping); + } + else + { + container = ApplyDefaultTypeMapping(container); + + if (container.TypeMapping is null) + { + throw new InvalidOperationException( + "Couldn't find container type mapping when applying container/containee mappings"); + } + } + } + + return (ApplyTypeMapping(container, containerMapping), ApplyTypeMapping(containee, containeeMapping)); + } + } + + private SqlExpression ApplyTypeMappingOnNewArray( + GaussDBNewArrayExpression pgNewArrayExpression, + RelationalTypeMapping? typeMapping) + { + var arrayTypeMapping = typeMapping as GaussDBArrayTypeMapping; + if (arrayTypeMapping is null && typeMapping is not null) + { + throw new ArgumentException($"Type mapping {typeMapping.GetType().Name} isn't an {nameof(GaussDBArrayTypeMapping)}"); + } + + RelationalTypeMapping? elementTypeMapping = null; + + // First, loop over the expressions to infer the array's type mapping (if not provided), and to make + // sure we don't have heterogeneous store types. + foreach (var expression in pgNewArrayExpression.Expressions) + { + if (expression.TypeMapping is not { } expressionTypeMapping) + { + continue; + } + + if (elementTypeMapping is null) + { + elementTypeMapping = expressionTypeMapping; + } + else if (expressionTypeMapping.StoreType != elementTypeMapping.StoreType) + { + // We have two heterogeneous store types in the array. + // We allow this when they have the same base type but differing facets (e.g. varchar(10) and varchar(15)), in which case + // we cast up. We also manually take care of some special cases (e.g. text and varchar(10) -> text). + // This is a hacky solution until a full type compatibility chart is implemented + // (https://github.com/dotnet/efcore/issues/15586) + if (expressionTypeMapping.StoreTypeNameBase == elementTypeMapping.StoreTypeNameBase) + { + if (expressionTypeMapping.Size is not null && elementTypeMapping.Size is not null) + { + var size = Math.Max(expressionTypeMapping.Size.Value, elementTypeMapping.Size.Value); + + elementTypeMapping = _typeMappingSource.FindMapping($"{expressionTypeMapping.StoreTypeNameBase}({size})"); + } + else if (expressionTypeMapping.Precision is not null + && elementTypeMapping.Precision is not null + && expressionTypeMapping.Scale is not null + && elementTypeMapping.Scale is not null) + { + var precision = Math.Max(expressionTypeMapping.Precision.Value, elementTypeMapping.Precision.Value); + var scale = Math.Max(expressionTypeMapping.Scale.Value, elementTypeMapping.Scale.Value); + + elementTypeMapping = + _typeMappingSource.FindMapping($"{expressionTypeMapping.StoreTypeNameBase}({precision},{scale})"); + } + else if (expressionTypeMapping.Precision is not null && elementTypeMapping.Precision is not null) + { + var precision = Math.Max(expressionTypeMapping.Precision.Value, elementTypeMapping.Precision.Value); + + elementTypeMapping = _typeMappingSource.FindMapping($"{expressionTypeMapping.StoreTypeNameBase}({precision})"); + } + } + else if (expressionTypeMapping.StoreType == "text" && IsTextualTypeMapping(elementTypeMapping)) + { + elementTypeMapping = expressionTypeMapping; + } + else if (elementTypeMapping.StoreType == "text" && IsTextualTypeMapping(expressionTypeMapping)) + { + // elementTypeMapping is already "text" + } + else + { + throw new InvalidOperationException( + GaussDBStrings.HeterogeneousTypesInNewArray( + elementTypeMapping.StoreType, expressionTypeMapping.StoreType)); + } + + static bool IsTextualTypeMapping(RelationalTypeMapping mapping) + => mapping.StoreTypeNameBase is "varchar" or "char" or "character varying" or "character" or "text"; + } + } + + // None of the array's expressions had a type mapping (i.e. no columns, only parameters/constants) + // Use the type mapping given externally + if (elementTypeMapping is null) + { + // No type mapping could be inferred from the expressions, nor was one given from the outside - + // we have no type mapping... Just return the original expression, which has no type mapping and will fail translation. + if (arrayTypeMapping is null) + { + return pgNewArrayExpression; + } + + elementTypeMapping = arrayTypeMapping.ElementTypeMapping; + } + else + { + // An element type mapping was successfully inferred from one of the expressions (there was a column). + // Infer the array's type mapping from it. + arrayTypeMapping = (GaussDBArrayTypeMapping?)_typeMappingSource.FindMapping( + pgNewArrayExpression.Type, + elementTypeMapping.StoreType + "[]"); + + // If the array's CLR type doesn't match the type mapping inferred from the element (e.g. CLR object[] with up-casted + // elements). Just return the original expression, which has no type mapping and will fail translation. + if (arrayTypeMapping is null) + { + return pgNewArrayExpression; + } + } + + // Now go over all expressions and apply the inferred element type mapping + List? newExpressions = null; + for (var i = 0; i < pgNewArrayExpression.Expressions.Count; i++) + { + var expression = pgNewArrayExpression.Expressions[i]; + var newExpression = ApplyTypeMapping(expression, elementTypeMapping); + if (newExpression != expression && newExpressions is null) + { + newExpressions = []; + for (var j = 0; j < i; j++) + { + newExpressions.Add(pgNewArrayExpression.Expressions[j]); + } + } + + newExpressions?.Add(newExpression); + } + + return new GaussDBNewArrayExpression( + newExpressions ?? pgNewArrayExpression.Expressions, + pgNewArrayExpression.Type, arrayTypeMapping); + } + + /// + /// GaussDB array indexing is 1-based. If the index happens to be a constant, + /// just increment it. Otherwise, append a +1 in the SQL. + /// + public virtual SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) + => expression is SqlConstantExpression constant + ? Constant(System.Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) + : Add(expression, Constant(1)); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBCompiledQueryCacheKeyGenerator.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBCompiledQueryCacheKeyGenerator.cs new file mode 100644 index 0000000000..f1951f7908 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBCompiledQueryCacheKeyGenerator.cs @@ -0,0 +1,56 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBCompiledQueryCacheKeyGenerator : RelationalCompiledQueryCacheKeyGenerator +{ + /// + /// 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 GaussDBCompiledQueryCacheKeyGenerator( + CompiledQueryCacheKeyGeneratorDependencies dependencies, + RelationalCompiledQueryCacheKeyGeneratorDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + /// 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 object GenerateCacheKey(Expression query, bool async) + => new GaussDBCompiledQueryCacheKey( + GenerateCacheKeyCore(query, async), + RelationalDependencies.ContextOptions.FindExtension()?.ReverseNullOrdering ?? false); + + private struct GaussDBCompiledQueryCacheKey( + RelationalCompiledQueryCacheKey relationalCompiledQueryCacheKey, + bool reverseNullOrdering) + { + private readonly RelationalCompiledQueryCacheKey _relationalCompiledQueryCacheKey = relationalCompiledQueryCacheKey; + private readonly bool _reverseNullOrdering = reverseNullOrdering; + + public override bool Equals(object? obj) + => !(obj is null) + && obj is GaussDBCompiledQueryCacheKey key + && Equals(key); + + private bool Equals(GaussDBCompiledQueryCacheKey other) + => _relationalCompiledQueryCacheKey.Equals(other._relationalCompiledQueryCacheKey) + && _reverseNullOrdering == other._reverseNullOrdering; + + public override int GetHashCode() + => HashCode.Combine(_relationalCompiledQueryCacheKey, _reverseNullOrdering); + } +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBDeleteConvertingExpressionVisitor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBDeleteConvertingExpressionVisitor.cs new file mode 100644 index 0000000000..7e70e56021 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBDeleteConvertingExpressionVisitor.cs @@ -0,0 +1,98 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; + +/// +/// Converts the relational into a PG-specific , which +/// precisely models a DELETE statement in GaussDB. This is done to handle the PG-specific USING syntax for table joining. +/// +public class GaussDBDeleteConvertingExpressionVisitor : 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. + /// + public virtual Expression Process(Expression node) + => node switch + { + DeleteExpression deleteExpression => VisitDelete(deleteExpression), + + _ => node + }; + + /// + /// 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 Expression VisitDelete(DeleteExpression deleteExpression) + { + var selectExpression = deleteExpression.SelectExpression; + + if (selectExpression.Offset != null + || selectExpression.Limit != null + || selectExpression.Having != null + || selectExpression.Orderings.Count > 0 + || selectExpression.GroupBy.Count > 0 + || selectExpression.Projection.Count > 0) + { + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( + nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); + } + + var fromItems = new List(); + SqlExpression? joinPredicates = null; + + // The SelectExpression also contains the target table being modified (same as deleteExpression.Table). + // If it has additional inner joins, use the GaussDB-specific USING syntax to express the join. + // Note that the non-join TableExpression isn't necessary the target table - through projection the last table being + // joined may be the one being modified. + foreach (var tableBase in selectExpression.Tables) + { + switch (tableBase) + { + case TableExpression tableExpression: + if (tableExpression.Alias != deleteExpression.Table.Alias) + { + fromItems.Add(tableExpression); + } + + break; + + case InnerJoinExpression { Table: { } tableExpression } innerJoinExpression: + if (tableExpression.Alias != deleteExpression.Table.Alias) + { + fromItems.Add(tableExpression); + } + + joinPredicates = joinPredicates is null + ? innerJoinExpression.JoinPredicate + : new SqlBinaryExpression( + ExpressionType.AndAlso, joinPredicates, innerJoinExpression.JoinPredicate, typeof(bool), + innerJoinExpression.JoinPredicate.TypeMapping); + break; + + default: + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( + nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); + } + } + + // Combine the join predicates (if any) before the user-provided predicate + var predicate = (joinPredicates, selectExpression.Predicate) switch + { + (null, not null) => selectExpression.Predicate, + (not null, null) => joinPredicates, + (null, null) => null, + (not null, not null) => new SqlBinaryExpression( + ExpressionType.AndAlso, joinPredicates, selectExpression.Predicate, typeof(bool), joinPredicates.TypeMapping) + }; + + return new GaussDBDeleteExpression(deleteExpression.Table, fromItems, predicate, deleteExpression.Tags); + } +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBEvaluatableExpressionFilter.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBEvaluatableExpressionFilter.cs new file mode 100644 index 0000000000..60476cf21d --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBEvaluatableExpressionFilter.cs @@ -0,0 +1,80 @@ +using System.Runtime.CompilerServices; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBEvaluatableExpressionFilter : RelationalEvaluatableExpressionFilter +{ + private readonly Version _postgresVersion; + + private static readonly MethodInfo TsQueryParse = + typeof(GaussDBTsQuery).GetRuntimeMethod(nameof(GaussDBTsQuery.Parse), [typeof(string)])!; + + private static readonly MethodInfo TsVectorParse = + typeof(GaussDBTsVector).GetRuntimeMethod(nameof(GaussDBTsVector.Parse), [typeof(string)])!; + + /// + /// 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 GaussDBEvaluatableExpressionFilter( + EvaluatableExpressionFilterDependencies dependencies, + RelationalEvaluatableExpressionFilterDependencies relationalDependencies, + IGaussDBSingletonOptions npgsqlSingletonOptions) + : base(dependencies, relationalDependencies) + { + _postgresVersion = npgsqlSingletonOptions.PostgresVersion; + } + + /// + /// 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 bool IsEvaluatableExpression(Expression expression, IModel model) + { + switch (expression) + { + case MethodCallExpression methodCallExpression: + var declaringType = methodCallExpression.Method.DeclaringType; + var method = methodCallExpression.Method; + + if (method == TsQueryParse + || method == TsVectorParse + || declaringType == typeof(GaussDBDbFunctionsExtensions) + || declaringType == typeof(GaussDBFullTextSearchDbFunctionsExtensions) + || declaringType == typeof(GaussDBFullTextSearchLinqExtensions) + || declaringType == typeof(GaussDBNetworkDbFunctionsExtensions) + || declaringType == typeof(GaussDBJsonDbFunctionsExtensions) + || declaringType == typeof(GaussDBCidrDbFunctionsExtensions) + // PG18 introduced uuidv7(), so we prevent local evaluation when targeting PG18 or later. + || declaringType == typeof(Guid) && method.Name == nameof(Guid.CreateVersion7) && _postgresVersion.AtLeast(18) + // Prevent evaluation of ValueTuple.Create, see NewExpression of ITuple below + || declaringType == typeof(ValueTuple) && method.Name == nameof(ValueTuple.Create)) + { + return false; + } + + break; + + case NewExpression newExpression when newExpression.Type.IsAssignableTo(typeof(ITuple)): + // We translate new ValueTuple(x, y...) to a SQL row value expression: (x, y) + // (see GaussDBSqlTranslatingExpressionVisitor.VisitNew). + // We must prevent evaluation when the tuple contains only constants/parameters, since SQL row values cannot be + // parameterized; we need to render them as "literals" instead: + // WHERE (x, y) > (3, $1) + return false; + } + + return base.IsEvaluatableExpression(expression, model); + } +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBParameterBasedSqlProcessor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBParameterBasedSqlProcessor.cs new file mode 100644 index 0000000000..9311d6f653 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBParameterBasedSqlProcessor.cs @@ -0,0 +1,42 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBParameterBasedSqlProcessor : RelationalParameterBasedSqlProcessor +{ + /// + /// 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 GaussDBParameterBasedSqlProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) + { + } + + ///// + ///// 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 Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade) + //{ + // queryExpression = base.Process(queryExpression, parametersFacade); + + // queryExpression = new GaussDBDeleteConvertingExpressionVisitor().Process(queryExpression); + + // return queryExpression; + //} + + ///// + //protected override Expression ProcessSqlNullability(Expression selectExpression, CacheSafeParameterFacade parametersFacade) + // => new GaussDBSqlNullabilityProcessor(Dependencies, Parameters).Process(selectExpression, parametersFacade); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBParameterBasedSqlProcessorFactory.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBParameterBasedSqlProcessorFactory.cs new file mode 100644 index 0000000000..f5fc587b79 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBParameterBasedSqlProcessorFactory.cs @@ -0,0 +1,32 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBParameterBasedSqlProcessorFactory : IRelationalParameterBasedSqlProcessorFactory +{ + private readonly RelationalParameterBasedSqlProcessorDependencies _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 + /// 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 GaussDBParameterBasedSqlProcessorFactory( + RelationalParameterBasedSqlProcessorDependencies dependencies) + { + _dependencies = dependencies; + } + + /// + /// Creates a new . + /// + /// Parameters for . + /// A relational parameter based sql processor. + public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + => new GaussDBParameterBasedSqlProcessor(_dependencies, parameters); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQueryCompilationContext.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryCompilationContext.cs new file mode 100644 index 0000000000..8bcd051723 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryCompilationContext.cs @@ -0,0 +1,51 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBQueryCompilationContext : RelationalQueryCompilationContext +{ + /// + /// 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 GaussDBQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async) + : this(dependencies, relationalDependencies, async, precompiling: false) + { + } + + /// + /// 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 GaussDBQueryCompilationContext( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies, + bool async, + bool precompiling) + : base(dependencies, relationalDependencies, async, precompiling) + { + } + + /// + /// 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 bool IsBuffering + => base.IsBuffering || QuerySplittingBehavior == Microsoft.EntityFrameworkCore.QuerySplittingBehavior.SplitQuery; + + /// + public override bool SupportsPrecompiledQuery => true; +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQueryCompilationContextFactory.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryCompilationContextFactory.cs new file mode 100644 index 0000000000..6bd46a8c0e --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryCompilationContextFactory.cs @@ -0,0 +1,48 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBQueryCompilationContextFactory : IQueryCompilationContextFactory +{ + private readonly QueryCompilationContextDependencies _dependencies; + private readonly RelationalQueryCompilationContextDependencies _relationalDependencies; + + /// + /// 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 GaussDBQueryCompilationContextFactory( + QueryCompilationContextDependencies dependencies, + RelationalQueryCompilationContextDependencies relationalDependencies) + { + Check.NotNull(dependencies, nameof(dependencies)); + Check.NotNull(relationalDependencies, nameof(relationalDependencies)); + + _dependencies = dependencies; + _relationalDependencies = relationalDependencies; + } + + /// + /// 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 Create(bool async) + => new GaussDBQueryCompilationContext(_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) + => new GaussDBQueryCompilationContext(_dependencies, _relationalDependencies, async, precompiling: true); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQuerySqlGenerator.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQuerySqlGenerator.cs new file mode 100644 index 0000000000..7b700a9849 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQuerySqlGenerator.cs @@ -0,0 +1,1586 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.NetworkInformation; +using System.Text.RegularExpressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; + +/// +/// The default query SQL generator for GaussDB. +/// +public class GaussDBQuerySqlGenerator : QuerySqlGenerator +{ + private readonly ISqlGenerationHelper _sqlGenerationHelper; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private RelationalTypeMapping? _textTypeMapping; + + /// + /// True if null ordering is reversed; otherwise false. + /// + private readonly bool _reverseNullOrderingEnabled; + + /// + /// The backend version to target. If null, it means the user hasn't set a compatibility version, and the + /// latest should be targeted. + /// + private readonly Version _postgresVersion; + + /// + public GaussDBQuerySqlGenerator( + QuerySqlGeneratorDependencies dependencies, + IRelationalTypeMappingSource typeMappingSource, + bool reverseNullOrderingEnabled, + Version postgresVersion) + : base(dependencies) + { + _sqlGenerationHelper = dependencies.SqlGenerationHelper; + _typeMappingSource = typeMappingSource; + _reverseNullOrderingEnabled = reverseNullOrderingEnabled; + _postgresVersion = postgresVersion; + } + + /// + /// 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 + { + GaussDBAllExpression e => VisitArrayAll(e), + GaussDBAnyExpression e => VisitArrayAny(e), + GaussDBArrayIndexExpression e => VisitArrayIndex(e), + GaussDBArraySliceExpression e => VisitArraySlice(e), + GaussDBBinaryExpression e => VisitPgBinary(e), + GaussDBDeleteExpression e => VisitPgDelete(e), + GaussDBFunctionExpression e => VisitPgFunction(e), + GaussDBILikeExpression e => VisitILike(e), + GaussDBJsonTraversalExpression e => VisitJsonPathTraversal(e), + GaussDBNewArrayExpression e => VisitNewArray(e), + GaussDBRegexMatchExpression e => VisitRegexMatch(e), + GaussDBRowValueExpression e => VisitRowValue(e), + GaussDBUnknownBinaryExpression e => VisitUnknownBinary(e), + GaussDBTableValuedFunctionExpression e => VisitPgTableValuedFunctionExpression(e), + + _ => base.VisitExtension(extensionExpression) + }; + + /// + protected override void GenerateRootCommand(Expression queryExpression) + { + switch (queryExpression) + { + case GaussDBDeleteExpression postgresDeleteExpression: + GenerateTagsHeaderComment(postgresDeleteExpression.Tags); + VisitPgDelete(postgresDeleteExpression); + break; + + default: + base.GenerateRootCommand(queryExpression); + break; + } + } + + /// + protected override void GenerateLimitOffset(SelectExpression selectExpression) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + + if (selectExpression.Limit is not null) + { + Sql.AppendLine().Append("LIMIT "); + Visit(selectExpression.Limit); + } + + if (selectExpression.Offset is not null) + { + if (selectExpression.Limit is null) + { + Sql.AppendLine(); + } + else + { + Sql.Append(" "); + } + + Sql.Append("OFFSET "); + Visit(selectExpression.Offset); + } + } + + /// + protected override string GetOperator(SqlBinaryExpression e) + => e.OperatorType switch + { + // GaussDB has a special string concatenation operator: || + // We switch to it if the expression itself has type string, or if one of the sides has a string type mapping. + // Same for full-text search's TsVector, arrays. + ExpressionType.Add when + e.Type == typeof(string) + || e.Left.TypeMapping?.ClrType == typeof(string) + || e.Right.TypeMapping?.ClrType == typeof(string) + || e.Type == typeof(GaussDBTsVector) + || e.Left.TypeMapping?.ClrType == typeof(GaussDBTsVector) + || e.Right.TypeMapping?.ClrType == typeof(GaussDBTsVector) + || e.Left.TypeMapping is GaussDBArrayTypeMapping && e.Right.TypeMapping is GaussDBArrayTypeMapping + => " || ", + + 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 GaussDB the caret is the exponentiation + // operator, and hash (#) is used instead. + ExpressionType.ExclusiveOr when e.Type == typeof(bool) => " <> ", + ExpressionType.ExclusiveOr => " # ", + + _ => base.GetOperator(e) + }; + + /// + protected override Expression VisitOrdering(OrderingExpression ordering) + { + var result = base.VisitOrdering(ordering); + + if (_reverseNullOrderingEnabled) + { + Sql.Append(ordering.IsAscending ? " NULLS FIRST" : " NULLS LAST"); + } + + return result; + } + + /// + protected override void GenerateTop(SelectExpression selectExpression) + { + // No TOP() in GaussDB, see GenerateLimitOffset + } + + /// + /// 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 VisitCrossApply(CrossApplyExpression crossApplyExpression) + { + Sql.Append("JOIN LATERAL "); + + if (crossApplyExpression.Table is TableExpression table) + { + // GaussDB doesn't support LATERAL JOIN over table, and it doesn't really make sense to do it - but EF Core + // will sometimes generate that. #1560 + Sql + .Append("(SELECT * FROM ") + .Append(_sqlGenerationHelper.DelimitIdentifier(table.Name, table.Schema)) + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(table.Alias)); + } + else + { + Visit(crossApplyExpression.Table); + } + + Sql.Append(" ON TRUE"); + return crossApplyExpression; + } + + /// + /// 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 VisitOuterApply(OuterApplyExpression outerApplyExpression) + { + Sql.Append("LEFT JOIN LATERAL "); + + if (outerApplyExpression.Table is TableExpression table) + { + // GaussDB doesn't support LATERAL JOIN over table, and it doesn't really make sense to do it - but EF Core + // will sometimes generate that. #1560 + Sql + .Append("(SELECT * FROM ") + .Append(_sqlGenerationHelper.DelimitIdentifier(table.Name, table.Schema)) + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(table.Alias)); + } + else + { + Visit(outerApplyExpression.Table); + } + + Sql.Append(" ON TRUE"); + return outerApplyExpression; + } + + /// + protected override Expression VisitSqlBinary(SqlBinaryExpression binary) + { + switch (binary.OperatorType) + { + case ExpressionType.Add: + { + if (_postgresVersion >= new Version(9, 5)) + { + return base.VisitSqlBinary(binary); + } + + // GaussDB 9.4 and below has some weird operator precedence fixed in 9.5 and described here: + // http://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=c6b3c939b7e0f1d35f4ed4996e71420a993810d2 + // As a result we must surround string concatenation with parentheses + if (binary.Left.Type == typeof(string) && binary.Right.Type == typeof(string)) + { + Sql.Append("("); + var exp = base.VisitSqlBinary(binary); + Sql.Append(")"); + return exp; + } + + return base.VisitSqlBinary(binary); + } + + case ExpressionType.ArrayIndex: + return VisitArrayIndex(binary); + + default: + return base.VisitSqlBinary(binary); + } + } + + // NonQueryConvertingExpressionVisitor converts the relational DeleteExpression to PostgresDeleteExpression, so we should never + // get here + /// + /// 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 VisitDelete(DeleteExpression deleteExpression) + => throw new InvalidOperationException("Inconceivable!"); + + /// + /// 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 Expression VisitPgDelete(GaussDBDeleteExpression pgDeleteExpression) + { + Sql.Append("DELETE FROM "); + Visit(pgDeleteExpression.Table); + + if (pgDeleteExpression.FromItems.Count > 0) + { + Sql.AppendLine().Append("USING "); + GenerateList(pgDeleteExpression.FromItems, t => Visit(t), sql => sql.Append(", ")); + } + + if (pgDeleteExpression.Predicate != null) + { + Sql.AppendLine().Append("WHERE "); + Visit(pgDeleteExpression.Predicate); + } + + return pgDeleteExpression; + } + + /// + /// 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 VisitUpdate(UpdateExpression updateExpression) + { + var selectExpression = updateExpression.SelectExpression; + + if (selectExpression.Offset == null + && selectExpression.Limit == null + && selectExpression.Having == null + && selectExpression.Orderings.Count == 0 + && selectExpression.GroupBy.Count == 0 + && selectExpression.Projection.Count == 0 + && (selectExpression.Tables.Count == 1 + || !ReferenceEquals(selectExpression.Tables[0], updateExpression.Table) + || selectExpression.Tables[1] is InnerJoinExpression + || selectExpression.Tables[1] is CrossJoinExpression)) + { + Sql.Append("UPDATE "); + Visit(updateExpression.Table); + Sql.AppendLine(); + Sql.Append("SET "); + Sql.Append( + $"{_sqlGenerationHelper.DelimitIdentifier(updateExpression.ColumnValueSetters[0].Column.Name)} = "); + Visit(updateExpression.ColumnValueSetters[0].Value); + using (Sql.Indent()) + { + foreach (var columnValueSetter in updateExpression.ColumnValueSetters.Skip(1)) + { + Sql.AppendLine(","); + Sql.Append($"{_sqlGenerationHelper.DelimitIdentifier(columnValueSetter.Column.Name)} = "); + Visit(columnValueSetter.Value); + } + } + + var predicate = selectExpression.Predicate; + var firstTable = true; + OuterReferenceFindingExpressionVisitor? visitor = null; + + if (selectExpression.Tables.Count > 1) + { + Sql.AppendLine().Append("FROM "); + + for (var i = 0; i < selectExpression.Tables.Count; i++) + { + var table = selectExpression.Tables[i]; + var joinExpression = table as JoinExpressionBase; + + if (updateExpression.Table.Alias == (joinExpression?.Table.Alias ?? table.Alias)) + { + LiftPredicate(table); + continue; + } + + visitor ??= new OuterReferenceFindingExpressionVisitor(updateExpression.Table); + + // GaussDB doesn't support referencing the main update table from anywhere except for the UPDATE WHERE clause. + // This specifically makes it impossible to have joins which reference the main table in their predicate (ON ...). + // Because of this, we detect all such inner joins and lift their predicates to the main WHERE clause (where a reference to the + // main table is allowed), producing UPDATE ... FROM x, y WHERE y.foreign_key = x.id instead of INNER JOIN ... ON. + if (firstTable) + { + LiftPredicate(table); + table = joinExpression?.Table ?? table; + } + else if (joinExpression is InnerJoinExpression innerJoinExpression + && visitor.ContainsReferenceToMainTable(innerJoinExpression.JoinPredicate)) + { + LiftPredicate(innerJoinExpression); + + Sql.AppendLine(","); + using (Sql.Indent()) + { + Visit(innerJoinExpression.Table); + } + + continue; + } + + if (firstTable) + { + firstTable = false; + } + else + { + Sql.AppendLine(); + } + + Visit(table); + + void LiftPredicate(TableExpressionBase joinTable) + { + if (joinTable is PredicateJoinExpressionBase predicateJoinExpression) + { + Check.DebugAssert(joinExpression is not LeftJoinExpression, "Cannot lift predicate for left join"); + + predicate = predicate == null + ? predicateJoinExpression.JoinPredicate + : new SqlBinaryExpression( + ExpressionType.AndAlso, + predicateJoinExpression.JoinPredicate, + predicate, + typeof(bool), + predicate.TypeMapping); + } + } + } + } + + if (predicate != null) + { + Sql.AppendLine().Append("WHERE "); + Visit(predicate); + } + + return updateExpression; + } + + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate))); + } + + /// + /// 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 Expression VisitNewArray(GaussDBNewArrayExpression pgNewArrayExpression) + { + Debug.Assert(pgNewArrayExpression.TypeMapping is not null); + + Sql.Append("ARRAY["); + var first = true; + foreach (var initializer in pgNewArrayExpression.Expressions) + { + if (!first) + { + Sql.Append(","); + } + + first = false; + Visit(initializer); + } + + // Not sure if the explicit store type is necessary, but just to be sure. + Sql + .Append("]::") + .Append(pgNewArrayExpression.TypeMapping.StoreType); + + return pgNewArrayExpression; + } + + /// + /// 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 Expression VisitPgBinary(GaussDBBinaryExpression binaryExpression) + { + Check.NotNull(binaryExpression, nameof(binaryExpression)); + + var requiresParentheses = RequiresParentheses(binaryExpression, binaryExpression.Left); + + if (requiresParentheses) + { + Sql.Append("("); + } + + Visit(binaryExpression.Left); + + if (requiresParentheses) + { + Sql.Append(")"); + } + + Debug.Assert(binaryExpression.Left.TypeMapping is not null); + Debug.Assert(binaryExpression.Right.TypeMapping is not null); + + Sql + .Append(" ") + .Append( + binaryExpression.OperatorType switch + { + GaussDBExpressionType.Contains + when binaryExpression.Left.TypeMapping is GaussDBInetTypeMapping or GaussDBCidrTypeMapping + => ">>", + + GaussDBExpressionType.ContainedBy + when binaryExpression.Left.TypeMapping is GaussDBInetTypeMapping or GaussDBCidrTypeMapping + => "<<", + + GaussDBExpressionType.Contains => "@>", + GaussDBExpressionType.ContainedBy => "<@", + GaussDBExpressionType.Overlaps => "&&", + + GaussDBExpressionType.NetworkContainedByOrEqual => "<<=", + GaussDBExpressionType.NetworkContainsOrEqual => ">>=", + GaussDBExpressionType.NetworkContainsOrContainedBy => "&&", + + GaussDBExpressionType.RangeIsStrictlyLeftOf => "<<", + GaussDBExpressionType.RangeIsStrictlyRightOf => ">>", + GaussDBExpressionType.RangeDoesNotExtendRightOf => "&<", + GaussDBExpressionType.RangeDoesNotExtendLeftOf => "&>", + GaussDBExpressionType.RangeIsAdjacentTo => "-|-", + GaussDBExpressionType.RangeUnion => "+", + GaussDBExpressionType.RangeIntersect => "*", + GaussDBExpressionType.RangeExcept => "-", + + GaussDBExpressionType.TextSearchMatch => "@@", + GaussDBExpressionType.TextSearchAnd => "&&", + GaussDBExpressionType.TextSearchOr => "||", + + GaussDBExpressionType.JsonExists => "?", + GaussDBExpressionType.JsonExistsAny => "?|", + GaussDBExpressionType.JsonExistsAll => "?&", + + GaussDBExpressionType.LTreeMatches + when binaryExpression.Right.TypeMapping.StoreType == "lquery" + || binaryExpression.Right.TypeMapping is GaussDBArrayTypeMapping { ElementTypeMapping.StoreType: "lquery" } => "~", + GaussDBExpressionType.LTreeMatches + when binaryExpression.Right.TypeMapping.StoreType == "ltxtquery" + => "@", + GaussDBExpressionType.LTreeMatchesAny => "?", + GaussDBExpressionType.LTreeFirstAncestor => "?@>", + GaussDBExpressionType.LTreeFirstDescendent => "?<@", + GaussDBExpressionType.LTreeFirstMatches + when binaryExpression.Right.TypeMapping.StoreType == "lquery" => "?~", + GaussDBExpressionType.LTreeFirstMatches + when binaryExpression.Right.TypeMapping.StoreType == "ltxtquery" => "?@", + + GaussDBExpressionType.Distance => "<->", + + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}") + }) + .Append(" "); + + requiresParentheses = RequiresParentheses(binaryExpression, binaryExpression.Right); + + if (requiresParentheses) + { + Sql.Append("("); + } + + Visit(binaryExpression.Right); + + if (requiresParentheses) + { + Sql.Append(")"); + } + + return binaryExpression; + } + + /// + /// 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 Expression VisitArrayIndex(SqlBinaryExpression expression) + { + Visit(expression.Left); + Sql.Append("["); + Visit(expression.Right); + Sql.Append("]"); + return 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 VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression) + { + Debug.Assert(sqlUnaryExpression.TypeMapping is not null); + Debug.Assert(sqlUnaryExpression.Operand.TypeMapping is not null); + + switch (sqlUnaryExpression.OperatorType) + { + case ExpressionType.Convert: + { + // GaussDB supports the standard CAST(x AS y), but also a lighter x::y which we use + // where there's no precedence issues + switch (sqlUnaryExpression.Operand) + { + case SqlConstantExpression: + case SqlParameterExpression: + case SqlUnaryExpression { OperatorType: ExpressionType.Convert }: + case ColumnExpression: + case SqlFunctionExpression: + case ScalarSubqueryExpression: + var storeType = sqlUnaryExpression.TypeMapping.StoreType switch + { + "integer" => "int", + "timestamp with time zone" => "timestamptz", + "timestamp without time zone" => "timestamp", + var s => s + }; + + Visit(sqlUnaryExpression.Operand); + Sql.Append("::"); + Sql.Append(storeType); + return sqlUnaryExpression; + } + + break; + } + + // Bitwise complement on networking types + case ExpressionType.Not when + sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(IPAddress) + || sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof((IPAddress, int)) + || sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(PhysicalAddress): + Sql.Append("~"); + Visit(sqlUnaryExpression.Operand); + return sqlUnaryExpression; + + // NOT operation on full-text queries + case ExpressionType.Not when sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(GaussDBTsQuery): + 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 GaussDBRegexMatchExpression regexMatch: + VisitRegexMatch(regexMatch, negated: true); + return sqlUnaryExpression; + + case GaussDBILikeExpression iLike: + VisitILike(iLike, negated: true); + return sqlUnaryExpression; + } + + break; + } + } + + return base.VisitSqlUnary(sqlUnaryExpression); + } + + /// + /// 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 void GenerateSetOperationOperand(SetOperationBase setOperation, SelectExpression operand) + { + // GaussDB allows ORDER BY and LIMIT in set operation operands, but requires parentheses + if (operand.Orderings.Count > 0 || operand.Limit is not null) + { + Sql.AppendLine("("); + using (Sql.Indent()) + { + Visit(operand); + } + + Sql.AppendLine().Append(")"); + return; + } + + base.GenerateSetOperationOperand(setOperation, operand); + } + + /// + /// 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 VisitCollate(CollateExpression collateExpression) + { + Check.NotNull(collateExpression, nameof(collateExpression)); + + Visit(collateExpression.Operand); + + // In PG, collation names are regular identifiers which need to be quoted for case-sensitivity. + Sql + .Append(" COLLATE ") + .Append(_sqlGenerationHelper.DelimitIdentifier(collateExpression.Collation)); + + return collateExpression; + } + + /// + /// 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 TryGenerateWithoutWrappingSelect(SelectExpression selectExpression) + // GaussDB supports VALUES as a top-level statement - and directly under set operations. + // However, when on the left side of a set operation, we need the column coming out of VALUES to be named, so we need the wrapping + // SELECT for that. + => selectExpression.Tables is not [ValuesExpression] + && base.TryGenerateWithoutWrappingSelect(selectExpression); + + /// + /// 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 void GenerateSetOperation(SetOperationBase setOperation) + { + GenerateSetOperationOperand(setOperation, setOperation.Source1); + + Sql + .AppendLine() + .Append( + setOperation switch + { + ExceptExpression => "EXCEPT", + IntersectExpression => "INTERSECT", + UnionExpression => "UNION", + _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType")) + }) + .AppendLine(setOperation.IsDistinct ? string.Empty : " ALL"); + + // For ValuesExpression, we can remove its wrapping SelectExpression but only if on the right side of a set operation, since on + // the left side we need the column name to be specified. + if (setOperation.Source2 is + { + Tables: [ValuesExpression valuesExpression], + Offset: null, + Limit: null, + IsDistinct: false, + Predicate: null, + Having: null, + Orderings.Count: 0, + GroupBy.Count: 0, + } rightSelectExpression + && rightSelectExpression.Projection.Count == valuesExpression.ColumnNames.Count + && rightSelectExpression.Projection.Select( + (pe, index) => pe.Expression is ColumnExpression column + && column.Name == valuesExpression.ColumnNames[index]) + .All(e => e)) + { + GenerateValues(valuesExpression); + } + else + { + GenerateSetOperationOperand(setOperation, setOperation.Source2); + } + } + + /// + /// 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 VisitValues(ValuesExpression valuesExpression) + { + base.VisitValues(valuesExpression); + + // GaussDB VALUES supports setting the projects column names: FROM (VALUES (1), (2)) AS v(foo) + Sql.Append("("); + + for (var i = 0; i < valuesExpression.ColumnNames.Count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i])); + } + + Sql.Append(")"); + + return valuesExpression; + } + + /// + /// 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 void GenerateValues(ValuesExpression valuesExpression) + { + if (valuesExpression.RowValues is null) + { + throw new UnreachableException(); + } + + if (valuesExpression.RowValues.Count == 0) + { + throw new InvalidOperationException(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + } + + // GaussDB 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. + var rowValues = valuesExpression.RowValues; + + Sql.Append("VALUES "); + + for (var i = 0; i < rowValues.Count; i++) + { + // TODO: Do we want newlines here? + if (i > 0) + { + Sql.Append(", "); + } + + Visit(valuesExpression.RowValues[i]); + } + } + + #region GaussDB-specific expression types + + /// + /// 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 Expression VisitArrayAll(GaussDBAllExpression expression) + { + Visit(expression.Item); + + Sql + .Append(" ") + .Append( + expression.OperatorType switch + { + PgAllOperatorType.Like => "LIKE", + PgAllOperatorType.ILike => "ILIKE", + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {expression.OperatorType}") + }) + .Append(" ALL ("); + + Visit(expression.Array); + + Sql.Append(")"); + + return 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 virtual Expression VisitArrayAny(GaussDBAnyExpression expression) + { + Visit(expression.Item); + + Sql + .Append(" ") + .Append( + expression.OperatorType switch + { + PgAnyOperatorType.Equal => "=", + PgAnyOperatorType.Like => "LIKE", + PgAnyOperatorType.ILike => "ILIKE", + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {expression.OperatorType}") + }) + .Append(" ANY ("); + + Visit(expression.Array); + + Sql.Append(")"); + + return expression; + } + + /// + /// Produces SQL array index expression (e.g. arr[1]). + /// + protected virtual Expression VisitArrayIndex(GaussDBArrayIndexExpression expression) + { + var requiresParentheses = RequiresParentheses(expression, expression.Array); + + if (requiresParentheses) + { + Sql.Append("("); + } + + Visit(expression.Array); + + if (requiresParentheses) + { + Sql.Append(")"); + } + + Sql.Append("["); + Visit(expression.Index); + Sql.Append("]"); + return expression; + } + + /// + /// Produces SQL array slice expression (e.g. arr[1:2]). + /// + protected virtual Expression VisitArraySlice(GaussDBArraySliceExpression expression) + { + var requiresParentheses = RequiresParentheses(expression, expression.Array); + + if (requiresParentheses) + { + Sql.Append("("); + } + + Visit(expression.Array); + + if (requiresParentheses) + { + Sql.Append(")"); + } + + Sql.Append("["); + Visit(expression.LowerBound); + Sql.Append(":"); + Visit(expression.UpperBound); + Sql.Append("]"); + return expression; + } + + /// + /// Produces SQL for GaussDB regex matching. + /// + /// + /// See: http://www.postgresql.org/docs/current/static/functions-matching.html + /// + protected virtual Expression VisitRegexMatch(GaussDBRegexMatchExpression expression, bool negated = false) + { + var options = expression.Options; + + Visit(expression.Match); + + if (options.HasFlag(RegexOptions.IgnoreCase)) + { + Sql.Append(negated ? " !~* " : " ~* "); + options &= ~RegexOptions.IgnoreCase; + } + else + { + Sql.Append(negated ? " !~ " : " ~ "); + } + + // PG regexps are single-line by default + if (options == RegexOptions.Singleline) + { + Visit(expression.Pattern); + return expression; + } + + var constantPattern = (expression.Pattern as SqlConstantExpression)?.Value as string; + + if (constantPattern is null) + { + Sql.Append("("); + } + + Sql.Append("'(?"); + + if (options.HasFlag(RegexOptions.Multiline)) + { + Sql.Append("n"); + } + else if (!options.HasFlag(RegexOptions.Singleline)) + { + // In .NET's default mode, . doesn't match newlines but in GaussDB it does. + Sql.Append("p"); + } + + if (options.HasFlag(RegexOptions.IgnorePatternWhitespace)) + { + Sql.Append("x"); + } + + Sql.Append(")"); + + if (constantPattern is null) + { + Sql.Append("' || "); + Visit(expression.Pattern); + Sql.Append(")"); + } + else + { + Sql.Append(constantPattern.Replace("'", "''")); + Sql.Append("'"); + } + + return 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 virtual Expression VisitRowValue(GaussDBRowValueExpression rowValueExpression) + { + Sql.Append("("); + + var values = rowValueExpression.Values; + var count = values.Count; + for (var i = 0; i < count; i++) + { + Visit(values[i]); + + if (i < count - 1) + { + Sql.Append(", "); + } + } + + Sql.Append(")"); + + return rowValueExpression; + } + + /// + /// 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 Expression VisitILike(GaussDBILikeExpression likeExpression, bool negated = false) + { + Visit(likeExpression.Match); + + if (negated) + { + Sql.Append(" NOT"); + } + + Sql.Append(" ILIKE "); + + Visit(likeExpression.Pattern); + + if (likeExpression.EscapeChar is not null) + { + Sql.Append(" ESCAPE "); + Visit(likeExpression.EscapeChar); + } + + return likeExpression; + } + + /// + /// 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 VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + { + // TODO: Stop producing empty JsonScalarExpressions, #30768 + var path = jsonScalarExpression.Path; + if (path.Count == 0) + { + Visit(jsonScalarExpression.Json); + return jsonScalarExpression; + } + + switch (jsonScalarExpression.TypeMapping) + { + // This case is for when a nested JSON entity is being accessed. We want the json/jsonb fragment in this case (not text), + // so we can perform further JSON operations on it. + case GaussDBOwnedJsonTypeMapping: + GenerateJsonPath(returnsText: false); + break; + + // No need to cast the output when we expect a string anyway + case StringTypeMapping: + GenerateJsonPath(returnsText: true); + break; + + // bytea requires special handling, since we encode the binary data as base64 inside the JSON, but that requires a special + // conversion function to be extracted out to a PG bytea. + case GaussDBByteArrayTypeMapping: + Sql.Append("decode("); + GenerateJsonPath(returnsText: true); + Sql.Append(", 'base64')"); + break; + + // Arrays require special handling; we cannot simply cast a JSON array (as text) to a PG array ([1,2,3] isn't a valid PG array + // representation). We use jsonb_array_elements_text to extract the array elements as a set, cast them to their PG element type + // and then build an array from that. + case GaussDBArrayTypeMapping arrayMapping: + Sql.Append("(ARRAY(SELECT CAST(element AS ").Append(arrayMapping.ElementTypeMapping.StoreType) + .Append(") FROM jsonb_array_elements_text("); + GenerateJsonPath(returnsText: false); + Sql.Append(") WITH ORDINALITY AS t(element) ORDER BY ordinality))"); + break; + + default: + Sql.Append("CAST("); + GenerateJsonPath(returnsText: true); + Sql.Append(" AS "); + Sql.Append(jsonScalarExpression.TypeMapping!.StoreType); + Sql.Append(")"); + break; + } + + return jsonScalarExpression; + + void GenerateJsonPath(bool returnsText) + => this.GenerateJsonPath( + jsonScalarExpression.Json, + returnsText: returnsText, + jsonScalarExpression.Path.Select( + s => s switch + { + { PropertyName: string propertyName } + => new SqlConstantExpression(propertyName, _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))), + { ArrayIndex: SqlExpression arrayIndex } => arrayIndex, + _ => throw new UnreachableException() + }).ToList()); + } + + /// + /// Visits the children of an . + /// + /// The expression. + /// + /// An . + /// + protected virtual Expression VisitJsonPathTraversal(GaussDBJsonTraversalExpression expression) + { + GenerateJsonPath(expression.Expression, expression.ReturnsText, expression.Path); + return expression; + } + + private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadOnlyList path) + { + Visit(expression); + + if (path.Count == 1) + { + Sql.Append(returnsText ? " ->> " : " -> "); + Visit(path[0]); + return; + } + + // Multiple path components + Sql.Append(returnsText ? " #>> " : " #> "); + + // Use simplified array literal syntax if all path components are constants for cleaner SQL + if (path.All(p => p is SqlConstantExpression { Value: var pathSegment } + && (pathSegment is not string s || s.All(char.IsAsciiLetterOrDigit)))) + { + Sql + .Append("'{") + .Append(string.Join(",", path.Select(p => ((SqlConstantExpression)p).Value))) + .Append("}'"); + } + else + { + Sql.Append("ARRAY["); + for (var i = 0; i < path.Count; i++) + { + Visit(path[i]); + if (i < path.Count - 1) + { + Sql.Append(","); + } + } + + Sql.Append("]::text[]"); + } + } + + /// + /// 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 Expression VisitPgTableValuedFunctionExpression(GaussDBTableValuedFunctionExpression tableValuedFunctionExpression) + { + // PostgresTableValuedFunctionExpression extends the standard TableValuedFunctionExpression, adding the possibility to specify + // column names and types at the end, as well as an optional WITH ORDINALITY to project an index out. + + // Note that GaussDB doesn't support specifying both a column definition (with type) *and* WITH ORDINALITY; but it does allow + // wrapping the function invocation and column definition inside ROWS FROM, and placing the table alias and WITH ORDINALITY outside. + // We take care of that here. + if (tableValuedFunctionExpression is + { + WithOrdinality: true, + ColumnInfos: { } columnInfos + } + && columnInfos.Any(ci => ci.TypeMapping is not null)) + { + Sql.Append("ROWS FROM ("); + + Sql.Append(tableValuedFunctionExpression.Name).Append("("); + GenerateList(tableValuedFunctionExpression.Arguments, e => Visit(e)); + Sql.Append(") AS "); + + GenerateColumnDefinition(); + + Sql.Append(") WITH ORDINALITY AS ").Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Alias)); + } + else + { + Sql.Append(tableValuedFunctionExpression.Name).Append("("); + GenerateList(tableValuedFunctionExpression.Arguments, e => Visit(e)); + Sql.Append(")"); + + if (tableValuedFunctionExpression.WithOrdinality) + { + Sql.Append(" WITH ORDINALITY"); + } + + Sql.Append(AliasSeparator).Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Alias)); + + if (tableValuedFunctionExpression.ColumnInfos is not null) + { + GenerateColumnDefinition(); + } + } + + return tableValuedFunctionExpression; + + void GenerateColumnDefinition() + { + Sql.Append("("); + + if (tableValuedFunctionExpression.ColumnInfos is [var singleColumnInfo]) + { + GenerateColumnInfo(singleColumnInfo); + } + else + { + Sql.AppendLine(); + using var _ = Sql.Indent(); + + for (var i = 0; i < tableValuedFunctionExpression.ColumnInfos.Count; i++) + { + var columnInfo = tableValuedFunctionExpression.ColumnInfos[i]; + + if (i > 0) + { + Sql.AppendLine(","); + } + + GenerateColumnInfo(columnInfo); + } + + Sql.AppendLine(); + } + + Sql.Append(")"); + + void GenerateColumnInfo(GaussDBTableValuedFunctionExpression.ColumnInfo columnInfo) + { + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(columnInfo.Name)); + + if (columnInfo.TypeMapping is not null) + { + Sql.Append(" ").Append(columnInfo.TypeMapping.StoreType); + } + } + } + } + + /// + /// Visits the children of a . + /// + /// The expression. + /// + /// An . + /// + protected virtual Expression VisitUnknownBinary(GaussDBUnknownBinaryExpression unknownBinaryExpression) + { + Check.NotNull(unknownBinaryExpression, nameof(unknownBinaryExpression)); + + var requiresParentheses = RequiresParentheses(unknownBinaryExpression, unknownBinaryExpression.Left); + + if (requiresParentheses) + { + Sql.Append("("); + } + + Visit(unknownBinaryExpression.Left); + + if (requiresParentheses) + { + Sql.Append(")"); + } + + Sql + .Append(" ") + .Append(unknownBinaryExpression.Operator) + .Append(" "); + + requiresParentheses = RequiresParentheses(unknownBinaryExpression, unknownBinaryExpression.Right); + + if (requiresParentheses) + { + Sql.Append("("); + } + + Visit(unknownBinaryExpression.Right); + + if (requiresParentheses) + { + Sql.Append(")"); + } + + return unknownBinaryExpression; + } + + /// + /// Visits the children of a . + /// + /// The expression. + /// + /// An . + /// + protected virtual Expression VisitPgFunction(GaussDBFunctionExpression e) + { + Check.NotNull(e, nameof(e)); + + if (e.IsBuiltIn) + { + Sql.Append(e.Name); + } + else + { + if (!string.IsNullOrEmpty(e.Schema)) + { + Sql + .Append(_sqlGenerationHelper.DelimitIdentifier(e.Schema)) + .Append("."); + } + + // TODO: Quote user-defined function names with upper-case (also for regular SqlFunctionExpression) + + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(e.Name)); + } + + Sql.Append("("); + + if (e.IsAggregateDistinct) + { + Sql.Append("DISTINCT "); + } + + for (var i = 0; i < e.Arguments.Count; i++) + { + if (i < e.ArgumentNames.Count && e.ArgumentNames[i] is { } argumentName) + { + Sql + .Append(argumentName) + .Append(" => "); + } + + Visit(e.Arguments[i]); + + if (i < e.Arguments.Count - 1) + { + Sql.Append( + i < e.ArgumentSeparators.Count && e.ArgumentSeparators[i] is not null + ? $" {e.ArgumentSeparators[i]} " + : ", "); + } + } + + if (e.AggregateOrderings.Count > 0) + { + Sql.Append(" ORDER BY "); + + for (var i = 0; i < e.AggregateOrderings.Count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Visit(e.AggregateOrderings[i]); + } + } + + Sql.Append(")"); + + if (e.AggregatePredicate is not null) + { + Sql.Append(" FILTER (WHERE "); + + Visit(e.AggregatePredicate); + + Sql.Append(")"); + } + + return e; + } + + #endregion GaussDB-specific expression types + + /// + /// 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 RequiresParentheses(SqlExpression outerExpression, SqlExpression innerExpression) + { + switch (innerExpression) + { + // PG doesn't support ~-, -~, ~~, -- so we add parentheses + case SqlUnaryExpression innerUnary when outerExpression is SqlUnaryExpression outerUnary + && (innerUnary.OperatorType is ExpressionType.Negate + || innerUnary.OperatorType is ExpressionType.Not && innerUnary.Type != typeof(bool)) + && (outerUnary.OperatorType is ExpressionType.Negate + || outerUnary.OperatorType is ExpressionType.Not && outerUnary.Type != typeof(bool)): + return true; + + // Copy paste of QuerySqlGenerator.RequiresParentheses for SqlBinaryExpression + case GaussDBBinaryExpression innerBinary: + { + // If the provider defined precedence for the two expression, use that + if (TryGetOperatorInfo(outerExpression, out var outerPrecedence, out var isOuterAssociative) + && TryGetOperatorInfo(innerExpression, out var innerPrecedence, out _)) + { + return outerPrecedence.CompareTo(innerPrecedence) switch + { + > 0 => true, + < 0 => false, + + // 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) + _ => outerExpression is not GaussDBBinaryExpression outerBinary + || outerBinary.OperatorType != innerBinary.OperatorType + || !isOuterAssociative + // Arithmetic operators on floating points aren't associative, because of rounding errors. + || outerExpression.Type == typeof(float) + || outerExpression.Type == typeof(double) + || innerExpression.Type == typeof(float) + || innerExpression.Type == typeof(double) + }; + } + + // Otherwise always parenthesize for safety + return true; + } + + case GaussDBUnknownBinaryExpression: + return true; + + default: + return base.RequiresParentheses(outerExpression, innerExpression); + } + } + + /// + /// 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 TryGetOperatorInfo(SqlExpression expression, out int precedence, out bool isAssociative) + { + // See https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-PRECEDENCE + (precedence, isAssociative) = expression switch + { + // TODO: Exponent => 1300 + + SqlBinaryExpression sqlBinaryExpression => sqlBinaryExpression.OperatorType switch + { + // Multiplication, division, modulo + ExpressionType.Multiply => (1200, true), + ExpressionType.Divide => (1200, false), + ExpressionType.Modulo => (1200, false), + + // Addition, subtraction (binary) + ExpressionType.Add => (1100, true), + ExpressionType.Subtract => (1100, false), + + // All other native and user-defined operators => 1000 + ExpressionType.LeftShift => (1000, true), + ExpressionType.RightShift => (1000, true), + ExpressionType.And when sqlBinaryExpression.Type != typeof(bool) => (1000, true), + ExpressionType.Or when sqlBinaryExpression.Type != typeof(bool) => (1000, true), + + // Comparison operators + ExpressionType.Equal => (800, false), + ExpressionType.NotEqual => (800, false), + ExpressionType.LessThan => (800, false), + ExpressionType.LessThanOrEqual => (800, false), + ExpressionType.GreaterThan => (800, false), + ExpressionType.GreaterThanOrEqual => (800, false), + + // Logical operators + ExpressionType.AndAlso => (500, true), + ExpressionType.OrElse => (500, true), + ExpressionType.And when sqlBinaryExpression.Type == typeof(bool) => (500, true), + ExpressionType.Or when sqlBinaryExpression.Type == typeof(bool) => (500, true), + + _ => default, + }, + + SqlUnaryExpression sqlUnaryExpression => sqlUnaryExpression.OperatorType switch + { + ExpressionType.Convert => (1600, false), + ExpressionType.Negate => (1400, false), + ExpressionType.Not when sqlUnaryExpression.Type != typeof(bool) => (1000, false), + ExpressionType.Equal => (700, false), // IS NULL + ExpressionType.NotEqual => (700, false), // IS NOT NULL + ExpressionType.Not when sqlUnaryExpression.Type == typeof(bool) => (600, false), + + _ => default, + }, + + // There's an "any other operator" category in the PG operator precedence table, we assign that a numeric value of 1000. + // TODO: Some operators here may be associative + GaussDBBinaryExpression => (1000, false), + + CollateExpression => (1000, false), + AtTimeZoneExpression => (1100, false), + InExpression => (900, false), + GaussDBJsonTraversalExpression => (1000, false), + GaussDBArrayIndexExpression => (1500, false), + GaussDBAllExpression or GaussDBAnyExpression => (800, false), + LikeExpression or GaussDBILikeExpression or GaussDBRegexMatchExpression => (900, false), + + _ => default, + }; + + return precedence != default; + } + + private void GenerateList( + IReadOnlyList items, + Action generationAction, + Action? joinAction = null) + { + joinAction ??= (isb => isb.Append(", ")); + + for (var i = 0; i < items.Count; i++) + { + if (i > 0) + { + joinAction(Sql); + } + + generationAction(items[i]); + } + } + + private sealed class OuterReferenceFindingExpressionVisitor(TableExpression mainTable) : ExpressionVisitor + { + private bool _containsReference; + + public bool ContainsReferenceToMainTable(SqlExpression sqlExpression) + { + _containsReference = false; + + Visit(sqlExpression); + + return _containsReference; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + if (_containsReference) + { + return expression; + } + + if (expression is ColumnExpression { TableAlias: var tableAlias } + && tableAlias == mainTable.Alias) + { + _containsReference = true; + + return expression; + } + + return base.Visit(expression); + } + } +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQuerySqlGeneratorFactory.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQuerySqlGeneratorFactory.cs new file mode 100644 index 0000000000..94f6b88005 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQuerySqlGeneratorFactory.cs @@ -0,0 +1,42 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; + +/// +/// The default factory for GaussDB-specific query SQL generators. +/// +public class GaussDBQuerySqlGeneratorFactory : IQuerySqlGeneratorFactory +{ + private readonly QuerySqlGeneratorDependencies _dependencies; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly IGaussDBSingletonOptions _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 GaussDBQuerySqlGeneratorFactory( + QuerySqlGeneratorDependencies dependencies, + IRelationalTypeMappingSource typeMappingSource, + IGaussDBSingletonOptions npgsqlSingletonOptions) + { + _dependencies = dependencies; + _typeMappingSource = typeMappingSource; + _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. + /// + public virtual QuerySqlGenerator Create() + => new GaussDBQuerySqlGenerator( + _dependencies, + _typeMappingSource, + _npgsqlSingletonOptions.ReverseNullOrderingEnabled, + _npgsqlSingletonOptions.PostgresVersion); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQueryTranslationPostprocessor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryTranslationPostprocessor.cs new file mode 100644 index 0000000000..85abab9568 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryTranslationPostprocessor.cs @@ -0,0 +1,60 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor +{ + private readonly GaussDBSqlTreePruner _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 + /// 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 GaussDBQueryTranslationPostprocessor( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies, + RelationalQueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + } + + /// + /// 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 Expression Process(Expression query) + { + var result = base.Process(query); + + result = new GaussDBUnnestPostprocessor().Visit(result); + result = new GaussDBSetOperationTypingInjector().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 GaussDBTypeMappingPostprocessor(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.GaussDB/Query/Internal/GaussDBQueryTranslationPostprocessorFactory.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryTranslationPostprocessorFactory.cs new file mode 100644 index 0000000000..09fd0f738d --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryTranslationPostprocessorFactory.cs @@ -0,0 +1,41 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBQueryTranslationPostprocessorFactory : IQueryTranslationPostprocessorFactory +{ + /// + /// 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 GaussDBQueryTranslationPostprocessorFactory( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual QueryTranslationPostprocessorDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalQueryTranslationPostprocessorDependencies RelationalDependencies { get; } + + /// + public virtual QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext) + => new GaussDBQueryTranslationPostprocessor( + Dependencies, + RelationalDependencies, + (RelationalQueryCompilationContext)queryCompilationContext); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryableMethodTranslatingExpressionVisitor.cs new file mode 100644 index 0000000000..8ffb0f3123 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryableMethodTranslatingExpressionVisitor.cs @@ -0,0 +1,862 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Extensions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor +{ + private readonly RelationalQueryCompilationContext _queryCompilationContext; + private readonly GaussDBTypeMappingSource _typeMappingSource; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly bool _isRedshift; + private RelationalTypeMapping? _ordinalityTypeMapping; + + #region MethodInfos + + private static readonly MethodInfo Like2MethodInfo = + typeof(DbFunctionsExtensions).GetRuntimeMethod( + nameof(DbFunctionsExtensions.Like), [typeof(DbFunctions), typeof(string), typeof(string)])!; + + // ReSharper disable once InconsistentNaming + private static readonly MethodInfo ILike2MethodInfo + = typeof(GaussDBDbFunctionsExtensions).GetRuntimeMethod( + nameof(GaussDBDbFunctionsExtensions.ILike), [typeof(DbFunctions), typeof(string), typeof(string)])!; + + private static readonly MethodInfo MatchesLQuery = + typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLQuery), [typeof(string)])!; + + #endregion + + /// + /// 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 GaussDBQueryableMethodTranslatingExpressionVisitor( + QueryableMethodTranslatingExpressionVisitorDependencies dependencies, + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, + RelationalQueryCompilationContext queryCompilationContext, + IGaussDBSingletonOptions GaussDBSingletonOptions) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + _queryCompilationContext = queryCompilationContext; + _typeMappingSource = (GaussDBTypeMappingSource)relationalDependencies.TypeMappingSource; + _sqlExpressionFactory = (GaussDBExpressionFactory)relationalDependencies.SqlExpressionFactory; + _isRedshift = GaussDBSingletonOptions.UseRedshift; + } + + /// + /// 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 GaussDBQueryableMethodTranslatingExpressionVisitor(GaussDBQueryableMethodTranslatingExpressionVisitor parentVisitor) + : base(parentVisitor) + { + _queryCompilationContext = parentVisitor._queryCompilationContext; + _typeMappingSource = parentVisitor._typeMappingSource; + _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; + _isRedshift = parentVisitor._isRedshift; + } + + /// + /// 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 QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() + => new GaussDBQueryableMethodTranslatingExpressionVisitor(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. + /// + 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; + + // If this is a collection property, get the element's nullability out of metadata. Otherwise, this is a parameter property, in + // which case we only have the CLR type (note that we cannot produce different SQLs based on the nullability of an *element* in + // a parameter collection - our caching mechanism only supports varying by the nullability of the parameter itself (i.e. the + // collection). + // TODO: if property is non-null, GetElementType() should never be null, but we have #31469 for shadow properties + var isElementNullable = property?.GetElementType() is null + ? elementClrType.IsNullableType() + : property.GetElementType()!.IsNullable; + + // We support two kinds of primitive collections: the standard one with PostgreSQL arrays (where we use the unnest function), and + // a special case for geometry collections, where we use + SelectExpression selectExpression; + + // 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 + // inferred". +#pragma warning disable EF1001 // SelectExpression constructors are pubternal + switch (sqlExpression.TypeMapping) + { + case { StoreTypeNameBase: "geometry" or "geography" }: + { + // TODO: For geometry collection support (not yet supported), see #2850. + selectExpression = new SelectExpression( + [new TableValuedFunctionExpression(tableAlias, "ST_Dump", [sqlExpression])], + new ColumnExpression("geom", tableAlias, elementClrType.UnwrapNullableType(), elementTypeMapping, isElementNullable), + identifier: [], // TODO + _queryCompilationContext.SqlAliasManager); + break; + } + + // Scalar/primitive collection mapped to a PostgreSQL array (typical and default case) + case GaussDBArrayTypeMapping or GaussDBMultirangeTypeMapping or null: + { + // Note that for unnest we have a special expression type extending TableValuedFunctionExpression, adding the ability to provide + // an explicit column name for its output (SELECT * FROM unnest(array) AS f(foo)). + // This is necessary since when the column name isn't explicitly specified, it is automatically identical to the table alias + // (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. 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 GaussDBUnnestExpression(tableAlias, sqlExpression, "value")], + new ColumnExpression( + "value", + tableAlias, + elementClrType.UnwrapNullableType(), + elementTypeMapping, + isElementNullable), + identifier: [(ordinalityColumn, ordinalityComparer)], + _queryCompilationContext.SqlAliasManager); + + selectExpression.AppendOrdering(new OrderingExpression(ordinalityColumn, ascending: true)); + break; + } + + // Scalar/primitive collection mapped to a JSON array, like in other providers. + // Happens for scalar collections nested within JSON documents, or if the user explicitly mapped to JSON instead of + // the default GaussDB array. + // Translate to SELECT element::int FROM jsonb_array_elements_text(...) WITH ORDINALITY + case GaussDBJsonTypeMapping { ElementTypeMapping: not null, StoreType: var storeType }: + { + var (ordinalityColumn, ordinalityComparer) = GenerateOrdinalityIdentifier(tableAlias); + + SqlExpression elementProjection = new ColumnExpression( + "element", + tableAlias, + typeof(string), + _typeMappingSource.FindMapping(typeof(string)), + isElementNullable); + + // If the projected type is anything other than a text, apply a cast (jsonb_array_elements_text returns text) + if (!elementTypeMapping!.StoreType.Equals("text", StringComparison.OrdinalIgnoreCase)) + { + elementProjection = _sqlExpressionFactory.Convert( + elementProjection, + elementClrType.UnwrapNullableType(), + elementTypeMapping); + } + + selectExpression = new SelectExpression( + [ + new GaussDBTableValuedFunctionExpression( + tableAlias, + storeType switch + { + "jsonb" => "jsonb_array_elements_text", + "json" => "json_array_elements_text", + _ => throw new UnreachableException() + }, + [sqlExpression], + columnInfos: [new("element")], + withOrdinality: true) + ], + elementProjection, + identifier: [(ordinalityColumn, ordinalityComparer)], + _queryCompilationContext.SqlAliasManager); + + selectExpression.AppendOrdering(new OrderingExpression(ordinalityColumn, ascending: true)); + break; + } + + default: + throw new UnreachableException(); + } +#pragma warning restore EF1001 // SelectExpression constructors are pubternal + + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); + + if (elementClrType != shaperExpression.Type) + { + Check.DebugAssert( + elementClrType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementClrType); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); + } + + /// + /// 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 TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression) + { + // 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 = + _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 + { + { TypeMapping.StoreType: "jsonb" } => "jsonb_to_recordset", + { TypeMapping.StoreType: "json" } => "json_to_recordset", + { TypeMapping: null } => throw new UnreachableException("Missing type mapping on JSON column"), + + _ => throw new UnreachableException() + }; + + var jsonTypeMapping = jsonQueryExpression.JsonColumn.TypeMapping!; + //Check.DebugAssert(jsonTypeMapping is GaussDBStructuralJsonTypeMapping, "JSON column has a non-JSON mapping"); + + // We now add all of projected entity's the properties and navigations into the jsonb_to_recordset's AS clause, which defines the + // names and types of columns to come out of the JSON fragments. + var columnInfos = new List(); + + // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys + foreach (var property in jsonQueryExpression.StructuralType.GetPropertiesInHierarchy()) + { + if (property.GetJsonPropertyName() is string jsonPropertyName) + { + columnInfos.Add( + new GaussDBTableValuedFunctionExpression.ColumnInfo + { + Name = jsonPropertyName, + TypeMapping = property.GetRelationalTypeMapping() + }); + } + } + + switch (jsonQueryExpression.StructuralType) + { + case IEntityType entityType: + foreach (var navigation in entityType.GetNavigationsInHierarchy() + .Where(n => n.ForeignKey.IsOwnership + && n.TargetEntityType.IsMappedToJson() + && n.ForeignKey.PrincipalToDependent == n)) + { + var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName(); + Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}"); + + columnInfos.Add( + new GaussDBTableValuedFunctionExpression.ColumnInfo { Name = jsonNavigationName, TypeMapping = jsonTypeMapping }); + } + + break; + + case IComplexType complexType: + foreach (var complexProperty in complexType.GetComplexProperties()) + { + var jsonPropertyName = complexProperty.ComplexType.GetJsonPropertyName(); + Check.DebugAssert(jsonPropertyName is not null, $"No JSON property name for complex property {complexProperty.Name}"); + + columnInfos.Add( + new GaussDBTableValuedFunctionExpression.ColumnInfo { Name = jsonPropertyName, TypeMapping = jsonTypeMapping }); + } + + break; + + default: + throw new UnreachableException(); + } + + // json_to_recordset requires the nested JSON document - it does not accept a path within a containing JSON document (like SQL + // Server OPENJSON or SQLite json_each). So we wrap json_to_recordset around a JsonScalarExpression which will extract the nested + // document. + var jsonScalarExpression = new JsonScalarExpression( + jsonQueryExpression.JsonColumn, jsonQueryExpression.Path, typeof(string), jsonTypeMapping, jsonQueryExpression.IsNullable); + + // Construct the json_to_recordset around the JsonScalarExpression, and wrap it in a SelectExpression + var jsonToRecordSetExpression = new GaussDBTableValuedFunctionExpression( + tableAlias, functionName, [jsonScalarExpression], columnInfos, withOrdinality: true); + +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var selectExpression = CreateSelect( + jsonQueryExpression, + jsonToRecordSetExpression, + "ordinality", + typeof(int), + _typeMappingSource.FindMapping(typeof(int))!); +#pragma warning restore EF1001 // Internal EF Core API usage. + + return new ShapedQueryExpression( + selectExpression, + new RelationalStructuralTypeShaperExpression( + jsonQueryExpression.StructuralType, + new ProjectionBindingExpression( + selectExpression, + new ProjectionMember(), + typeof(ValueBuffer)), + false)); + } + + /// + /// 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 TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) + { + // 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.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( + projectedColumn1.TypeMapping is not null || projectedColumn2.TypeMapping is not null, + "Concat with no type mapping on either side (operation should be client-evaluated over parameters/constants"); + + // TODO: Conflicting type mappings from both sides? + var inferredTypeMapping = projectedColumn1.TypeMapping ?? projectedColumn2.TypeMapping; + +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source1.QueryExpression).Tables.Single().Alias!; + var selectExpression = new SelectExpression( + [new GaussDBUnnestExpression(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 + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), source1.ShaperExpression.Type.MakeNullable()); + + if (source1.ShaperExpression.Type != shaperExpression.Type) + { + Check.DebugAssert( + source1.ShaperExpression.Type.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, source1.ShaperExpression.Type); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); + } + + return base.TranslateConcat(source1, source2); + } + + /// + /// 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? TranslateSkip(ShapedQueryExpression source, Expression count) + { + // 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.TryExtractArray(out var array, out var projectedColumn) + && TranslateExpression(count) is { } translatedCount) + { +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; + var selectExpression = new SelectExpression( + [ + new GaussDBUnnestExpression( + 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( + 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.TranslateSkip(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? TranslateTake(ShapedQueryExpression source, Expression count) + { + // 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.TryExtractArray(out var array, out var projectedColumn) + && TranslateExpression(count) is { } translatedCount) + { + GaussDBArraySliceExpression sliceExpression; + + // If Skip has been called before, an array slice expression is already there; try to integrate this Take into it. + // Note that we need to take the Skip (lower bound) into account for the Take (upper bound), since the slice upper bound + // operates on the original array (Skip hasn't yet taken place). + if (array is GaussDBArraySliceExpression existingSliceExpression) + { + if (existingSliceExpression is + { + LowerBound: SqlConstantExpression { Value: int lowerBoundValue } lowerBound, + UpperBound: null + }) + { + sliceExpression = existingSliceExpression.Update( + existingSliceExpression.Array, + existingSliceExpression.LowerBound, + translatedCount is SqlConstantExpression { Value: int takeCount } + ? _sqlExpressionFactory.Constant(lowerBoundValue + takeCount - 1, lowerBound.TypeMapping) + : _sqlExpressionFactory.Subtract( + _sqlExpressionFactory.Add(lowerBound, translatedCount), + _sqlExpressionFactory.Constant(1, lowerBound.TypeMapping))); + } + else + { + // For any other case, we allow relational to translate with normal querying. For non-constant lower bounds, we could + // duplicate them into the upper bound, but that could cause expensive double evaluation. + return base.TranslateTake(source, count); + } + } + else + { + sliceExpression = _sqlExpressionFactory.ArraySlice( + array, + lowerBound: null, + upperBound: translatedCount, + projectedColumn.IsNullable); + } + +#pragma warning disable EF1001 // SelectExpression constructors are currently internal + var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; + var selectExpression = new SelectExpression( + [new GaussDBUnnestExpression(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 + 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.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 GaussDBUnnestExpression(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: [GaussDBUnnestExpression unnest, ..] } + && (selectExpression.Orderings is [] + || selectExpression.Orderings is + [{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }] + && orderingTableAlias == unnest.Alias); + + #region ExecuteUpdate + + /// + /// 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 IsValidSelectExpressionForExecuteUpdate( + SelectExpression selectExpression, + TableExpressionBase targetTable, + [NotNullWhen(true)] out TableExpression? tableExpression) + { + if (!base.IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out tableExpression)) + { + return false; + } + + // PostgreSQL doesn't support referencing the main update table from anywhere except for the UPDATE WHERE clause. + // This specifically makes it impossible to have joins which reference the main table in their predicate (ON ...). + // Because of this, we detect all such inner joins and lift their predicates to the main WHERE clause (where a reference to the + // main table is allowed) - see GaussDBQuerySqlGenerator.VisitUpdate. + // For any other type of join which contains a reference to the main table, we return false to trigger a subquery pushdown instead. + OuterReferenceFindingExpressionVisitor? visitor = null; + + for (var i = 0; i < selectExpression.Tables.Count; i++) + { + var table = selectExpression.Tables[i]; + + if (ReferenceEquals(table, tableExpression)) + { + continue; + } + + visitor ??= new OuterReferenceFindingExpressionVisitor(tableExpression); + + // For inner joins, if the predicate contains a reference to the main table, GaussDBQuerySqlGenerator will lift the predicate + // to the WHERE clause; so we only need to check the inner join's table (i.e. subquery) for such a reference. + // Cross join and cross/outer apply (lateral joins) don't have predicates, so just check the entire join for a reference to + // the main table, and switch to subquery syntax if one is found. + // Left join does have a predicate, but it isn't possible to lift it to the main WHERE clause; so also check the entire + // join. + if (table is InnerJoinExpression innerJoin) + { + table = innerJoin.Table; + } + + if (visitor.ContainsReferenceToMainTable(table)) + { + return false; + } + } + + return true; + } + +#pragma warning disable EF9002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + /// + /// 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 TrySerializeScalarToJson( + JsonScalarExpression target, + SqlExpression value, + [NotNullWhen(true)] out SqlExpression? jsonValue) + { + var jsonTypeMapping = ((ColumnExpression)target.Json).TypeMapping!; + + if ( + // The base implementation doesn't handle serializing arbitrary SQL expressions to JSON, since that's + // database-specific. In PostgreSQL we simply do this by wrapping any expression in to_jsonb(). + !base.TrySerializeScalarToJson(target, value, out jsonValue) + // In addition, for string, numeric and bool, the base implementation simply returns the value as-is, since most databases allow + // passing these native types directly to their JSON partial update function. In PostgreSQL, jsonb_set() always requires jsonb, + // so we wrap those expression with to_jsonb() as well. + || jsonValue.TypeMapping?.StoreType is not "jsonb" and not "json") + { + switch (value.TypeMapping!.StoreType) + { + case "jsonb" or "json": + jsonValue = value; + return true; + + case "bytea": + value = _sqlExpressionFactory.Function( + "encode", + [value, _sqlExpressionFactory.Constant("base64")], + nullable: true, + argumentsPropagateNullability: [true, true], + typeof(string), + _typeMappingSource.FindMapping(typeof(string))! + ); + break; + } + + // We now have a scalar value expression that needs to be passed to jsonb_set(), but jsonb_set() requires a json/jsonb + // argument, not e.g. text or int. So we need to wrap the argument in to_jsonb/to_json. + // Note that for structural types we always already get a jsonb/json value and have already exited above (no need for + // to_jsonb/to_json). + + // One exception is if the value expression happens to be a JsonScalarExpression (e.g. copy scalar property from within + // one JSON document into another). For that case, rather than do to_jsonb(x.JsonbDoc ->> 'SomeProperty') - which extracts + // a jsonb property as text only to reconvert it back to jsonb - we just change the type mapping on the JsonScalarExpression + // to json/jsonb, in order to generate x.JsonbDoc -> 'SomeProperty' (no text extraction). + if (value is JsonScalarExpression jsonScalarValue + && jsonScalarValue.Json.TypeMapping?.StoreType == jsonTypeMapping.StoreType) + { + jsonValue = new JsonScalarExpression( + jsonScalarValue.Json, + jsonScalarValue.Path, + jsonScalarValue.Type, + jsonTypeMapping, + jsonScalarValue.IsNullable); + return true; + } + + jsonValue = _sqlExpressionFactory.Function( + jsonTypeMapping.StoreType switch + { + "jsonb" => "to_jsonb", + "json" => "to_json", + _ => throw new UnreachableException() + }, + // Make sure GaussDB interprets constant values correctly by adding explicit typing based on the target property's type mapping. + // Note that we can only be here for scalar properties, for structural types we always already get a jsonb/json value + // and don't need to add to_jsonb/to_json. + [value is SqlConstantExpression ? _sqlExpressionFactory.Convert(value, target.Type, target.TypeMapping) : value], + nullable: true, + argumentsPropagateNullability: [true], + typeof(string), + jsonTypeMapping); + } + + return true; + } +#pragma warning restore EF9002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + /// + /// 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 SqlExpression? GenerateJsonPartialUpdateSetter( + Expression target, + SqlExpression value, + ref SqlExpression? existingSetterValue) + { + var (jsonColumn, path) = target switch + { + JsonScalarExpression j => ((ColumnExpression)j.Json, j.Path), + JsonQueryExpression j => (j.JsonColumn, j.Path), + + _ => throw new UnreachableException(), + }; + + var jsonSet = _sqlExpressionFactory.Function( + jsonColumn.TypeMapping?.StoreType switch + { + "jsonb" => "jsonb_set", + "json" => "json_set", + _ => throw new UnreachableException() + }, + arguments: + [ + existingSetterValue ?? jsonColumn, + // Hack: Rendering of JSONPATH strings happens in value generation. We can have a special expression for modify to hold the + // IReadOnlyList (just like Json{Scalar,Query}Expression), but instead we do the slight hack of packaging it + // as a constant argument; it will be unpacked and handled in SQL generation. + _sqlExpressionFactory.Constant(path, RelationalTypeMapping.NullMapping), + value + ], + nullable: true, + argumentsPropagateNullability: [true, true, true], + typeof(string), + jsonColumn.TypeMapping); + + if (existingSetterValue is null) + { + return jsonSet; + } + else + { + existingSetterValue = jsonSet; + return null; + } + } + + #endregion ExecuteUpdate + + /// + /// 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 IsValidSelectExpressionForExecuteDelete(SelectExpression selectExpression) + // The default relational behavior is to allow only single-table expressions, and the only permitted feature is a predicate. + // Here we extend this to also inner joins to tables, which we generate via the PostgreSQL-specific USING construct. + => selectExpression is + { + Orderings: [], + Offset: null, + Limit: null, + GroupBy: [], + Having: null + } + && selectExpression.Tables[0] is TableExpression && selectExpression.Tables.Skip(1).All(t => t is InnerJoinExpression); + + // PostgreSQL unnest is guaranteed to return output rows in the same order as its input array, + // https://www.postgresql.org/docs/current/functions-array.html. + /// + protected override bool IsOrdered(SelectExpression selectExpression) + => base.IsOrdered(selectExpression) + || selectExpression.Tables is + [GaussDBTableValuedFunctionExpression { Name: "unnest" or "jsonb_to_recordset" or "json_to_recordset" }]; + + private (ColumnExpression, ValueComparer) GenerateOrdinalityIdentifier(string tableAlias) + { + _ordinalityTypeMapping ??= _typeMappingSource.FindMapping("int")!; + return (new ColumnExpression("ordinality", tableAlias, typeof(int), _ordinalityTypeMapping, nullable: false), + _ordinalityTypeMapping.Comparer); + } + + /// + /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, just increment it. Otherwise, append a +1 in the + /// SQL. + /// + private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) + => expression is SqlConstantExpression constant + ? _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( + new SelectExpression(translation, _queryCompilationContext.SqlAliasManager), + Expression.Convert( + new ProjectionBindingExpression(translation, new ProjectionMember(), typeof(bool?)), typeof(bool))); +#pragma warning restore EF1001 + + private sealed class OuterReferenceFindingExpressionVisitor(TableExpression mainTable) : ExpressionVisitor + { + private bool _containsReference; + + public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) + { + _containsReference = false; + + Visit(tableExpression); + + return _containsReference; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + if (_containsReference) + { + return expression; + } + + if (expression is ColumnExpression { TableAlias: var tableAlias } && tableAlias == mainTable.Alias) + { + _containsReference = true; + + return expression; + } + + return base.Visit(expression); + } + } +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryableMethodTranslatingExpressionVisitorFactory.cs new file mode 100644 index 0000000000..9c899bc1d9 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -0,0 +1,59 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory +{ + private readonly IGaussDBSingletonOptions _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 GaussDBQueryableMethodTranslatingExpressionVisitorFactory( + QueryableMethodTranslatingExpressionVisitorDependencies dependencies, + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, + IGaussDBSingletonOptions npgsqlSingletonOptions) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + _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 virtual QueryableMethodTranslatingExpressionVisitorDependencies Dependencies { 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. + /// + protected virtual RelationalQueryableMethodTranslatingExpressionVisitorDependencies 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 QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) + => new GaussDBQueryableMethodTranslatingExpressionVisitor( + Dependencies, + RelationalDependencies, + (RelationalQueryCompilationContext)queryCompilationContext, + _npgsqlSingletonOptions); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBSetOperationTypingInjector.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBSetOperationTypingInjector.cs new file mode 100644 index 0000000000..62b9c04700 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBSetOperationTypingInjector.cs @@ -0,0 +1,80 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; + +/// +/// A visitor that injects explicit typing on null projections in set operations, to ensure GaussDB gets the typing right. +/// +/// +/// +/// See the +/// GaussDB 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 GaussDBSetOperationTypingInjector : 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.GaussDB/Query/Internal/GaussDBSqlNullabilityProcessor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlNullabilityProcessor.cs new file mode 100644 index 0000000000..8797d2fe90 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlNullabilityProcessor.cs @@ -0,0 +1,741 @@ +using System.Runtime.CompilerServices; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; + +/// +public class GaussDBSqlNullabilityProcessor : SqlNullabilityProcessor +{ + 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 GaussDBSqlNullabilityProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) + { + _sqlExpressionFactory = dependencies.SqlExpressionFactory; + } + + /// + protected override SqlExpression VisitSqlBinary( + SqlBinaryExpression sqlBinaryExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + return sqlBinaryExpression switch + { + { + OperatorType: ExpressionType.Equal or ExpressionType.NotEqual, + Left: GaussDBRowValueExpression leftRowValue, + Right: GaussDBRowValueExpression rightRowValue + } + => VisitRowValueComparison(sqlBinaryExpression.OperatorType, leftRowValue, rightRowValue, out nullable), + + _ => base.VisitSqlBinary(sqlBinaryExpression, allowOptimizedExpansion, out nullable) + }; + + SqlExpression VisitRowValueComparison( + ExpressionType operatorType, + GaussDBRowValueExpression leftRowValue, + GaussDBRowValueExpression rightRowValue, + out bool 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 GaussDB 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; + + 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); + } + + visitedLeftValues?.Add(visitedLeftValue); + + if (visitedRightValue != rightValue && visitedRightValues is null) + { + visitedRightValues = SliceToList(rightRowValue.Values, count, i); + } + + visitedRightValues?.Add(visitedRightValue); + + 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, typeMapping: null, existingExpression: sqlBinaryExpression)!, + allowOptimizedExpansion, + out _); + + 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; + } + else + { + expandedExpression = operatorType switch + { + ExpressionType.Equal => _sqlExpressionFactory.AndAlso(expandedExpression, valueBinaryExpression), + ExpressionType.NotEqual => _sqlExpressionFactory.OrElse(expandedExpression, valueBinaryExpression), + _ => throw new UnreachableException() + }; + } + } + + // TODO: This is wrong, need to properly calculate this: #3250 + 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 + : _sqlExpressionFactory.MakeBinary( + operatorType, + visitedLeftValues is null + ? leftRowValue + : new GaussDBRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), + visitedRightValues is null + ? rightRowValue + : new GaussDBRowValueExpression(visitedRightValues, leftRowValue.Type, leftRowValue.TypeMapping), + typeMapping: null, + existingExpression: sqlBinaryExpression)!; + } + + 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) + { + return expandedExpression; + } + + // 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, + new GaussDBRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), + new GaussDBRowValueExpression(visitedRightValues, rightRowValue.Type, rightRowValue.TypeMapping), + typeMapping: null)!; + + // Technically the CLR type and type mappings are incorrect, as we're truncating the row values. + // But that shouldn't matter. + 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); + + for (var i = 0; i < count; i++) + { + list.Add(source[i]); + } + + return list; + } + } + } + + /// + protected override SqlExpression VisitCustomSqlExpression( + SqlExpression sqlExpression, + bool allowOptimizedExpansion, + out bool nullable) + => sqlExpression switch + { + GaussDBAnyExpression e => VisitAny(e, allowOptimizedExpansion, out nullable), + GaussDBAllExpression e => VisitAll(e, allowOptimizedExpansion, out nullable), + GaussDBArrayIndexExpression e => VisitArrayIndex(e, allowOptimizedExpansion, out nullable), + GaussDBArraySliceExpression e => VisitArraySlice(e, allowOptimizedExpansion, out nullable), + GaussDBBinaryExpression e => VisitPostgresBinary(e, allowOptimizedExpansion, out nullable), + GaussDBILikeExpression e => VisitILike(e, allowOptimizedExpansion, out nullable), + GaussDBJsonTraversalExpression e => VisitJsonTraversal(e, allowOptimizedExpansion, out nullable), + GaussDBNewArrayExpression e => VisitNewArray(e, allowOptimizedExpansion, out nullable), + GaussDBRegexMatchExpression e => VisitRegexMatch(e, allowOptimizedExpansion, out nullable), + GaussDBRowValueExpression e => VisitRowValueExpression(e, allowOptimizedExpansion, out nullable), + GaussDBUnknownBinaryExpression e => VisitUnknownBinary(e, allowOptimizedExpansion, out nullable), + + // PostgresFunctionExpression is visited via the SqlFunctionExpression override below + + _ => base.VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) + }; + + /// + /// 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 SqlExpression VisitAny(GaussDBAnyExpression anyExpression, bool allowOptimizedExpansion, out bool nullable) + { + Check.NotNull(anyExpression, nameof(anyExpression)); + + var item = Visit(anyExpression.Item, out var itemNullable); + var array = Visit(anyExpression.Array, out var entireArrayNullable); + + SqlExpression updated = anyExpression.Update(item, array); + + if (UseRelationalNulls) + { + nullable = false; + return updated; + } + + // We only do null compensation for item = ANY(array), not for any other operator. + // Note that LIKE (without ANY) doesn't get null-compensated either, so we're consistent with that. + if (anyExpression.OperatorType != PgAnyOperatorType.Equal) + { + // If the array is a constant and it contains a null, or if the array isn't a constant, + // we assume the entire expression can be null. + nullable = itemNullable || entireArrayNullable || MayContainNulls(anyExpression.Array); + return updated; + } + + // For PostgresAnyOperatorType.Equal, we perform null compensation. + // When the array is a parameter, we don't look at the values (in contrast to relational's + // VisitIn which expands parameter arrays into constants). + + // Note that ANY does not use GIN/GIST indexes on the array, but it does use indexes on the item. + // All of the below expansions allow index use on the item. + + // non_nullable = ANY(array) -> non_nullable = ANY(array) (optimized) + // non_nullable = ANY(array) -> non_nullable = ANY(array) AND (non_nullable = ANY(array) IS NOT NULL) (full) + // nullable = ANY(array) -> nullable = ANY(array) OR (nullable IS NULL AND array_position(array, NULL) IS NOT NULL) (optimized) + // nullable = ANY(array) -> (nullable = ANY(array) AND (nullable = ANY(array) IS NOT NULL)) OR (nullable IS NULL AND array_position(array, NULL) IS NOT NULL) (full) + + nullable = false; + + // ANY returns NULL if an element isn't found in the array but the array contains NULL, instead of false. + // So for non-optimized, we compensate by adding a check that NULL isn't returned. + if (!allowOptimizedExpansion) + { + updated = _sqlExpressionFactory.And(updated, _sqlExpressionFactory.IsNotNull(updated)); + } + + if (!itemNullable) + { + return updated; + } + + // If the item is nullable, add an OR to check for the item being null and the array containing null. + // The latter check is done with array_position, which returns null when a value was not found, and + // a position if the item (including null!) was found (IS NOT DISTINCT FROM semantics) + return _sqlExpressionFactory.OrElse( + updated, + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNull(item), + _sqlExpressionFactory.IsNotNull( + _sqlExpressionFactory.Function( + "array_position", + [array, _sqlExpressionFactory.Constant(null, item.Type, item.TypeMapping)], + nullable: true, + argumentsPropagateNullability: FalseArrays[2], + 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. + /// + protected virtual SqlExpression VisitAll(GaussDBAllExpression allExpression, bool allowOptimizedExpansion, out bool nullable) + { + Check.NotNull(allExpression, nameof(allExpression)); + + var item = Visit(allExpression.Item, out var itemNullable); + var array = Visit(allExpression.Array, out var entireArrayNullable); + + SqlExpression updated = allExpression.Update(item, array); + + if (UseRelationalNulls) + { + nullable = false; + return updated; + } + + // If the array is a constant and it contains a null, or if the array isn't a constant, + // we assume the entire expression can be null. + nullable = itemNullable || entireArrayNullable || MayContainNulls(allExpression.Array); + return updated; + } + + /// + /// 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 SqlExpression VisitArrayIndex( + GaussDBArrayIndexExpression arrayIndexExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(arrayIndexExpression, nameof(arrayIndexExpression)); + + var array = Visit(arrayIndexExpression.Array, allowOptimizedExpansion, out var arrayNullable); + var index = Visit(arrayIndexExpression.Index, allowOptimizedExpansion, out var indexNullable); + + nullable = arrayNullable || indexNullable || arrayIndexExpression.IsNullable; + + return arrayIndexExpression.Update(array, index); + } + + /// + /// 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 SqlExpression VisitArraySlice( + GaussDBArraySliceExpression arraySliceExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(arraySliceExpression, nameof(arraySliceExpression)); + + var array = Visit(arraySliceExpression.Array, allowOptimizedExpansion, out var arrayNullable); + var lowerBound = Visit(arraySliceExpression.LowerBound, allowOptimizedExpansion, out var lowerBoundNullable); + var upperBound = Visit(arraySliceExpression.UpperBound, allowOptimizedExpansion, out var upperBoundNullable); + + nullable = arrayNullable || lowerBoundNullable || upperBoundNullable || arraySliceExpression.IsNullable; + + return arraySliceExpression.Update(array, lowerBound, upperBound); + } + + /// + /// 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 SqlExpression VisitPostgresBinary( + GaussDBBinaryExpression binaryExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(binaryExpression, nameof(binaryExpression)); + + // For arrays Contains (arr1 @> arr2), we do not implement null semantics because doing so would prevent + // index use. So '{1, 2, NULL}' @> '{NULL}' returns false. This is notably the translation for .NET + // Contains over column arrays. + + var left = Visit(binaryExpression.Left, allowOptimizedExpansion, out var leftNullable); + var right = Visit(binaryExpression.Right, allowOptimizedExpansion, out var rightNullable); + + nullable = binaryExpression.OperatorType switch + { + // The following LTree search methods return null for "not found" + GaussDBExpressionType.LTreeFirstAncestor => true, + GaussDBExpressionType.LTreeFirstDescendent => true, + GaussDBExpressionType.LTreeFirstMatches => true, + + _ => leftNullable || rightNullable + }; + + return binaryExpression.Update(left, right); + } + + /// + /// 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 SqlExpression VisitSqlFunction( + SqlFunctionExpression sqlFunctionExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + // PostgresFunctionExpression extends SqlFunctionExpression, and adds aggregate predicate and ordering expressions to that. + // First call the base VisitSqlFunction to visit the arguments + var visitedBase = base.VisitSqlFunction(sqlFunctionExpression, allowOptimizedExpansion, out nullable); + + // base.VisitSqlFunction has some special logic for SUM which wraps it in a COALESCE + // (see https://github.com/dotnet/efcore/issues/28158), so we need some special handling to properly visit the + // PostgresFunctionExpression it wraps. + if (sqlFunctionExpression.IsBuiltIn + && string.Equals(sqlFunctionExpression.Name, "SUM", StringComparison.OrdinalIgnoreCase) + && visitedBase is SqlFunctionExpression { Name: "COALESCE", Arguments: { } } coalesceExpression + && coalesceExpression.Arguments[0] is GaussDBFunctionExpression wrappedFunctionExpression) + { + // The base logic assumes sum is operating over numbers, which breaks sum over PG interval. + // Detect that case and remove the coalesce entirely (note that we don't need coalescing since sum function is in + // EF.Functions.Sum, and returns nullable. This is a temporary hack until #38158 is fixed. + if (sqlFunctionExpression.Type == typeof(TimeSpan) + || sqlFunctionExpression.Type.FullName is "NodaTime.Period" or "NodaTime.Duration") + { + return coalesceExpression.Arguments[0]; + } + + var visitedArguments = coalesceExpression.Arguments!.ToArray(); + visitedArguments[0] = VisitPostgresFunctionComponents(wrappedFunctionExpression); + + return coalesceExpression.Update(coalesceExpression.Instance, visitedArguments); + } + + return visitedBase is GaussDBFunctionExpression pgFunctionExpression + ? VisitPostgresFunctionComponents(pgFunctionExpression) + : visitedBase; + + GaussDBFunctionExpression VisitPostgresFunctionComponents(GaussDBFunctionExpression pgFunctionExpression) + { + var aggregateChanged = false; + + var visitedAggregatePredicate = Visit(pgFunctionExpression.AggregatePredicate, allowOptimizedExpansion: true, out _); + aggregateChanged |= visitedAggregatePredicate != pgFunctionExpression.AggregatePredicate; + + OrderingExpression[]? visitedOrderings = null; + for (var i = 0; i < pgFunctionExpression.AggregateOrderings.Count; i++) + { + var ordering = pgFunctionExpression.AggregateOrderings[i]; + var visitedOrdering = ordering.Update(Visit(ordering.Expression, out _)); + if (visitedOrdering != ordering && visitedOrderings is null) + { + visitedOrderings = new OrderingExpression[pgFunctionExpression.AggregateOrderings.Count]; + for (var j = 0; j < i; j++) + { + visitedOrderings[j] = pgFunctionExpression.AggregateOrderings[j]; + } + + aggregateChanged = true; + } + + if (visitedOrderings is not null) + { + visitedOrderings[i] = visitedOrdering; + } + } + + return aggregateChanged + ? pgFunctionExpression.UpdateAggregateComponents( + visitedAggregatePredicate, + visitedOrderings ?? pgFunctionExpression.AggregateOrderings) + : pgFunctionExpression; + } + } + + /// + /// 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 SqlExpression VisitILike(GaussDBILikeExpression iLikeExpression, bool allowOptimizedExpansion, out bool nullable) + { + // 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; + + return result; + } + + nullable = false; + + //// 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; + } + + /// + /// 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 SqlExpression VisitNewArray( + GaussDBNewArrayExpression newArrayExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(newArrayExpression, nameof(newArrayExpression)); + + SqlExpression[]? newInitializers = null; + for (var i = 0; i < newArrayExpression.Expressions.Count; i++) + { + var initializer = newArrayExpression.Expressions[i]; + var newInitializer = Visit(initializer, allowOptimizedExpansion, out _); + if (newInitializer != initializer && newInitializers is null) + { + newInitializers = new SqlExpression[newArrayExpression.Expressions.Count]; + for (var j = 0; j < i; j++) + { + newInitializers[j] = newArrayExpression.Expressions[j]; + } + } + + if (newInitializers is not null) + { + newInitializers[i] = newInitializer; + } + } + + nullable = false; + return newInitializers is null + ? newArrayExpression + : new GaussDBNewArrayExpression(newInitializers, newArrayExpression.Type, newArrayExpression.TypeMapping); + } + + /// + /// 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 SqlExpression VisitRegexMatch( + GaussDBRegexMatchExpression regexMatchExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(regexMatchExpression, nameof(regexMatchExpression)); + + var match = Visit(regexMatchExpression.Match, out var matchNullable); + var pattern = Visit(regexMatchExpression.Pattern, out var patternNullable); + + nullable = matchNullable || patternNullable; + + return regexMatchExpression.Update(match, pattern); + } + + /// + /// 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 SqlExpression VisitJsonTraversal( + GaussDBJsonTraversalExpression jsonTraversalExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(jsonTraversalExpression, nameof(jsonTraversalExpression)); + + var expression = Visit(jsonTraversalExpression.Expression, out _); + + SqlExpression[]? newPath = null; + for (var i = 0; i < jsonTraversalExpression.Path.Count; i++) + { + var pathComponent = jsonTraversalExpression.Path[i]; + var newPathComponent = Visit(pathComponent, allowOptimizedExpansion, out _); + if (newPathComponent != pathComponent && newPath is null) + { + newPath = new SqlExpression[jsonTraversalExpression.Path.Count]; + for (var j = 0; j < i; j++) + { + newPath[j] = jsonTraversalExpression.Path[j]; + } + } + + if (newPath is not null) + { + newPath[i] = newPathComponent; + } + } + + // For now, anything inside a JSON document is considered nullable. + // See #1851 for optimizing this for JSON POCO mapping. + nullable = true; + + return jsonTraversalExpression.Update(expression, newPath?.ToArray() ?? jsonTraversalExpression.Path); + } + + /// + /// 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 SqlExpression VisitRowValueExpression( + GaussDBRowValueExpression rowValueExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + SqlExpression[]? newValues = null; + + for (var i = 0; i < rowValueExpression.Values.Count; i++) + { + var value = rowValueExpression.Values[i]; + + // Note that we disallow optimized expansion, since the null vs. false distinction does matter inside the row's values + var newValue = Visit(value, allowOptimizedExpansion: false, out _); + if (newValue != value && newValues is null) + { + newValues = new SqlExpression[rowValueExpression.Values.Count]; + for (var j = 0; j < i; j++) + { + newValues[j] = rowValueExpression.Values[j]; + } + } + + if (newValues is not null) + { + newValues[i] = newValue; + } + } + + // The row value expression itself can never be null + nullable = false; + + return rowValueExpression.Update(newValues ?? rowValueExpression.Values); + } + + /// + /// Visits a and computes its nullability. + /// + /// + /// + /// A expression to visit. + /// A bool value indicating if optimized expansion which considers null value as false value is allowed. + /// A bool value indicating whether the sql expression is nullable. + /// An optimized sql expression. + protected virtual SqlExpression VisitUnknownBinary( + GaussDBUnknownBinaryExpression unknownBinaryExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + Check.NotNull(unknownBinaryExpression, nameof(unknownBinaryExpression)); + + var left = Visit(unknownBinaryExpression.Left, allowOptimizedExpansion, out var leftNullable); + var right = Visit(unknownBinaryExpression.Right, allowOptimizedExpansion, out var rightNullable); + + nullable = leftNullable || rightNullable; + + return unknownBinaryExpression.Update(left, right); + } + + private static bool MayContainNulls(SqlExpression arrayExpression) + { + if (arrayExpression is SqlConstantExpression { Value: Array constantArray }) + { + for (var i = 0; i < constantArray.Length; i++) + { + if (constantArray.GetValue(i) is null) + { + return true; + } + } + + return false; + } + + 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 } && ParametersFacade.IsParameterNull(parameterName); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTranslatingExpressionVisitor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTranslatingExpressionVisitor.cs new file mode 100644 index 0000000000..f7123ae3b6 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTranslatingExpressionVisitor.cs @@ -0,0 +1,769 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.ExpressionTranslators.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using static HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor +{ + private readonly QueryCompilationContext _queryCompilationContext; + private readonly GaussDBExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly GaussDBJsonPocoTranslator _jsonPocoTranslator; + + private readonly RelationalTypeMapping _timestampMapping; + private readonly RelationalTypeMapping _timestampTzMapping; + + private static Type? _nodaTimePeriodType; + + private static readonly ConstructorInfo DateTimeCtor1 = + typeof(DateTime).GetConstructor([typeof(int), typeof(int), typeof(int)])!; + + private static readonly ConstructorInfo DateTimeCtor2 = + typeof(DateTime).GetConstructor([typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int)])!; + + private static readonly ConstructorInfo DateTimeCtor3 = + typeof(DateTime).GetConstructor( + [typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(DateTimeKind)])!; + + private static readonly ConstructorInfo DateOnlyCtor = + typeof(DateOnly).GetConstructor([typeof(int), typeof(int), typeof(int)])!; + + private static readonly MethodInfo StringStartsWithMethod + = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), [typeof(string)])!; + + private static readonly MethodInfo StringEndsWithMethod + = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), [typeof(string)])!; + + private static readonly MethodInfo StringContainsMethod + = typeof(string).GetRuntimeMethod(nameof(string.Contains), [typeof(string)])!; + + private static readonly MethodInfo EscapeLikePatternParameterMethod = + typeof(GaussDBSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!; + + // Note: This is the GaussDB default and does not need to be explicitly specified + private const char LikeEscapeChar = '\\'; + + /// + /// 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 GaussDBSqlTranslatingExpressionVisitor( + RelationalSqlTranslatingExpressionVisitorDependencies dependencies, + QueryCompilationContext queryCompilationContext, + QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) + : base(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor) + { + _queryCompilationContext = queryCompilationContext; + _sqlExpressionFactory = (GaussDBExpressionFactory)dependencies.SqlExpressionFactory; + _jsonPocoTranslator = ((GaussDBMemberTranslatorProvider)Dependencies.MemberTranslatorProvider).JsonPocoTranslator; + _typeMappingSource = dependencies.TypeMappingSource; + _timestampMapping = _typeMappingSource.FindMapping("timestamp without time zone")!; + _timestampTzMapping = _typeMappingSource.FindMapping("timestamp with time zone")!; + } + + /// + /// 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 VisitConditional(ConditionalExpression conditionalExpression) + { + var test = Visit(conditionalExpression.Test); + var ifTrue = Visit(conditionalExpression.IfTrue); + var ifFalse = Visit(conditionalExpression.IfFalse); + + if (TranslationFailed(conditionalExpression.Test, test, out var sqlTest) + || TranslationFailed(conditionalExpression.IfTrue, ifTrue, out var sqlIfTrue) + || TranslationFailed(conditionalExpression.IfFalse, ifFalse, out var sqlIfFalse)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + // Translate: + // a == b ? null : a -> NULLIF(a, b) + // a != b ? a : null -> NULLIF(a, b) + if (sqlTest is SqlBinaryExpression binary && sqlIfTrue is not null && sqlIfFalse is not null) + { + switch (binary.OperatorType) + { + case ExpressionType.Equal + when ifTrue is SqlConstantExpression { Value: null } && TryTranslateToNullIf(sqlIfFalse, out var nullIfTranslation): + case ExpressionType.NotEqual + when ifFalse is SqlConstantExpression { Value: null } && TryTranslateToNullIf(sqlIfTrue, out nullIfTranslation): + return nullIfTranslation; + } + } + + return _sqlExpressionFactory.Case([new CaseWhenClause(sqlTest!, sqlIfTrue!)], sqlIfFalse); + + bool TryTranslateToNullIf(SqlExpression conditionalResult, [NotNullWhen(true)] out Expression? nullIfTranslation) + { + var (left, right) = (binary.Left, binary.Right); + + if (left.Equals(conditionalResult)) + { + nullIfTranslation = _sqlExpressionFactory.Function( + "NULLIF", [left, right], true, [false, false], left.Type, left.TypeMapping); + return true; + } + + if (right.Equals(conditionalResult)) + { + nullIfTranslation = _sqlExpressionFactory.Function( + "NULLIF", [right, left], true, [false, false], right.Type, right.TypeMapping); + return true; + } + + nullIfTranslation = null; + return false; + } + } + + /// + /// 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 VisitUnary(UnaryExpression unaryExpression) + { + switch (unaryExpression.NodeType) + { + case ExpressionType.ArrayLength: + if (TranslationFailed(unaryExpression.Operand, Visit(unaryExpression.Operand), out var sqlOperand)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + // Translate Length on byte[], but only if the type mapping is for bytea. + // For byte[] mapped to an actual PG array (smallint[]), that's a primitive collection, and ArrayLength gets transformed to + // Count() which gets translated to cardinality() as usual in GaussDBQueryableMethodTranslatingExpressionVisitor. + if (sqlOperand!.Type == typeof(byte[]) && sqlOperand.TypeMapping is GaussDBByteArrayTypeMapping or null) + { + return _sqlExpressionFactory.Function( + "length", + [sqlOperand], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + typeof(int)); + } + + // Attempt to translate Length on a JSON POCO array + if (_jsonPocoTranslator.TranslateArrayLength(sqlOperand) is SqlExpression translation) + { + return translation; + } + + // Note that Length over PG arrays (not within JSON) gets translated by QueryableMethodTranslatingEV, since arrays are + // primitive collections + break; + + // We have row value comparison methods such as EF.Functions.GreaterThan, which accept two ValueTuples/Tuples. + // Since they accept ITuple parameters, the arguments have a Convert node casting up from the concrete argument to ITuple; + // this node causes translation failure in RelationalSqlTranslatingExpressionVisitor, so unwrap here. + case ExpressionType.Convert + when unaryExpression.Type == typeof(ITuple) && unaryExpression.Operand.Type.IsAssignableTo(typeof(ITuple)): + return Visit(unaryExpression.Operand); + + // We map both IPAddress and GaussDBInet to PG inet, and translate many methods accepting GaussDBInet, so ignore casts from + // IPAddress to GaussDBInet. + // On the GaussDB side, cidr is also implicitly convertible to inet, and at the ADO.NET level GaussDBCidr has a similar + // implicit conversion operator to GaussDBInet. So remove that cast as well. + case ExpressionType.Convert + when unaryExpression.Type == typeof(GaussDBInet) + && (unaryExpression.Operand.Type == typeof(IPAddress) || unaryExpression.Operand.Type == typeof(Metadata.GaussDBRange)): + return Visit(unaryExpression.Operand); + } + + return base.VisitUnary(unaryExpression); + } + + /// + /// 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 VisitNewArray(NewArrayExpression newArrayExpression) + { + if (base.VisitNewArray(newArrayExpression) is SqlExpression visitedNewArrayExpression) + { + return visitedNewArrayExpression; + } + + if (newArrayExpression.NodeType == ExpressionType.NewArrayInit) + { + var visitedExpressions = new SqlExpression[newArrayExpression.Expressions.Count]; + for (var i = 0; i < newArrayExpression.Expressions.Count; i++) + { + if (Visit(newArrayExpression.Expressions[i]) is SqlExpression visited) + { + visitedExpressions[i] = visited; + } + else + { + return QueryCompilationContext.NotTranslatedExpression; + } + } + + return _sqlExpressionFactory.NewArray(visitedExpressions, newArrayExpression.Type); + } + + return QueryCompilationContext.NotTranslatedExpression; + } + + /// + /// 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 VisitBinary(BinaryExpression binaryExpression) + { + switch (binaryExpression.NodeType) + { + case ExpressionType.Subtract + when binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate" + && binaryExpression.Right.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate": + { + if (TranslationFailed(binaryExpression.Left, Visit(TryRemoveImplicitConvert(binaryExpression.Left)), out var sqlLeft) + || TranslationFailed(binaryExpression.Right, Visit(TryRemoveImplicitConvert(binaryExpression.Right)), out var sqlRight)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + var subtraction = _sqlExpressionFactory.MakeBinary( + ExpressionType.Subtract, sqlLeft!, sqlRight!, _typeMappingSource.FindMapping(typeof(int)))!; + + return GaussDBFunctionExpression.CreateWithNamedArguments( + "make_interval", + [subtraction], + ["days"], + nullable: true, + argumentsPropagateNullability: TrueArrays[1], + builtIn: true, + _nodaTimePeriodType ??= binaryExpression.Left.Type.Assembly.GetType("NodaTime.Period")!, + typeMapping: null); + + // Note: many other date/time arithmetic operators are fully supported as-is by GaussDB - see GaussDBSqlExpressionFactory + } + + case ExpressionType.ArrayIndex: + { + // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see GaussDBArrayTranslator + Check.DebugFail( + "During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see GaussDBArrayTranslator. " + + "Should never see ArrayIndex."); + break; + } + } + + var translation = base.VisitBinary(binaryExpression); + + 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: GaussDBOwnedJsonTypeMapping } operand + } unary: + { + return unary.Update( + new JsonScalarExpression( + operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable)); + } + } + + return translation; + } + + /// + /// 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 VisitMethodCall(MethodCallExpression methodCallExpression) + { + var method = methodCallExpression.Method; + + if (method == StringStartsWithMethod + && TryTranslateStartsEndsWithContains( + methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.StartsWith, out var translation1)) + { + return translation1; + } + + if (method == StringEndsWithMethod + && TryTranslateStartsEndsWithContains( + methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.EndsWith, out var translation2)) + { + return translation2; + } + + if (method == StringContainsMethod + && TryTranslateStartsEndsWithContains( + methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.Contains, out var translation3)) + { + return translation3; + } + + return base.VisitMethodCall(methodCallExpression); + } + + /// + /// 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 VisitNew(NewExpression newExpression) + { + var visitedNewExpression = base.VisitNew(newExpression); + + if (visitedNewExpression != QueryCompilationContext.NotTranslatedExpression) + { + return visitedNewExpression; + } + + // We translate new ValueTuple(x, y...) to a SQL row value expression: (x, y). + // This is notably done to support row value comparisons: WHERE (x, y) > (3, 4) (see e.g. GaussDBDbFunctionsExtensions.GreaterThan) + if (newExpression.Type.IsAssignableTo(typeof(ITuple))) + { + return TryTranslateArguments(out var sqlArguments) + ? new GaussDBRowValueExpression(sqlArguments, newExpression.Type) + : QueryCompilationContext.NotTranslatedExpression; + } + + // Translate new DateTime(...) -> make_timestamp/make_date + if (newExpression.Constructor?.DeclaringType == typeof(DateTime)) + { + if (newExpression.Constructor == DateTimeCtor1) + { + return TryTranslateArguments(out var sqlArguments) + ? _sqlExpressionFactory.Function( + "make_date", sqlArguments, nullable: true, TrueArrays[3], typeof(DateTime), _timestampMapping) + : QueryCompilationContext.NotTranslatedExpression; + } + + if (newExpression.Constructor == DateTimeCtor2) + { + if (!TryTranslateArguments(out var sqlArguments)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + // DateTime's second component is an int, but GaussDB's MAKE_TIMESTAMP accepts a double precision + sqlArguments[5] = _sqlExpressionFactory.Convert(sqlArguments[5], typeof(double)); + + return _sqlExpressionFactory.Function( + "make_timestamp", sqlArguments, nullable: true, TrueArrays[6], typeof(DateTime), _timestampMapping); + } + + if (newExpression.Constructor == DateTimeCtor3 + && newExpression.Arguments[6] is ConstantExpression { Value : DateTimeKind kind }) + { + if (!TryTranslateArguments(out var sqlArguments)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + // DateTime's second component is an int, but GaussDB's make_timestamp/make_timestamptz accepts a double precision. + // Also chop off the last Kind argument which does not get sent to GaussDB + var rewrittenArguments = new List + { + sqlArguments[0], + sqlArguments[1], + sqlArguments[2], + sqlArguments[3], + sqlArguments[4], + _sqlExpressionFactory.Convert(sqlArguments[5], typeof(double)) + }; + + if (kind == DateTimeKind.Utc) + { + rewrittenArguments.Add(_sqlExpressionFactory.Constant("UTC")); + } + + return _sqlExpressionFactory.Function( + kind == DateTimeKind.Utc ? "make_timestamptz" : "make_timestamp", + rewrittenArguments, + nullable: true, + TrueArrays[rewrittenArguments.Count], + typeof(DateTime), + kind == DateTimeKind.Utc ? _timestampTzMapping : _timestampMapping); + } + } + + // Translate new DateOnly(...) -> make_date + if (newExpression.Constructor == DateOnlyCtor) + { + return TryTranslateArguments(out var sqlArguments) + ? _sqlExpressionFactory.Function( + "make_date", sqlArguments, nullable: true, TrueArrays[3], typeof(DateOnly)) + : QueryCompilationContext.NotTranslatedExpression; + } + + return QueryCompilationContext.NotTranslatedExpression; + + bool TryTranslateArguments(out SqlExpression[] sqlArguments) + { + sqlArguments = new SqlExpression[newExpression.Arguments.Count]; + for (var i = 0; i < sqlArguments.Length; i++) + { + var argument = newExpression.Arguments[i]; + if (TranslationFailed(argument, Visit(argument), out var sqlArgument)) + { + return false; + } + + sqlArguments[i] = sqlArgument!; + } + + return true; + } + } + + #region StartsWith/EndsWith/Contains + + private bool TryTranslateStartsEndsWithContains( + Expression instance, + Expression pattern, + StartsEndsWithContains methodType, + [NotNullWhen(true)] out SqlExpression? translation) + { + if (Visit(instance) is not SqlExpression translatedInstance + || Visit(pattern) is not SqlExpression translatedPattern) + { + translation = null; + return false; + } + + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(translatedInstance, translatedPattern); + + translatedInstance = _sqlExpressionFactory.ApplyTypeMapping(translatedInstance, stringTypeMapping); + translatedPattern = _sqlExpressionFactory.ApplyTypeMapping(translatedPattern, stringTypeMapping); + + switch (translatedPattern) + { + case SqlConstantExpression patternConstant: + { + // The pattern is constant. Aside from null and empty string, we escape all special characters (%, _, \) and send a + // simple LIKE + translation = patternConstant.Value switch + { + 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. + // Note that we don't just return a true constant, since null strings shouldn't match even an empty string + // (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable) + "" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")), + + string s => _sqlExpressionFactory.Like( + translatedInstance, + _sqlExpressionFactory.Constant( + methodType switch + { + StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', + StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), + StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", + + _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) + })), + + _ => throw new UnreachableException() + }; + + return true; + } + + case SqlParameterExpression patternParameter: + { + // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where + // all special characters have been escaped. + var lambda = Expression.Lambda( + Expression.Call( + EscapeLikePatternParameterMethod, + QueryCompilationContext.QueryContextParameter, + Expression.Constant(patternParameter.Name), + Expression.Constant(methodType)), + QueryCompilationContext.QueryContextParameter); + + var escapedPatternParameter = + _queryCompilationContext.RegisterRuntimeParameter( + $"{patternParameter.Name}_{methodType.ToString().ToLower(CultureInfo.InvariantCulture)}", lambda); + + translation = _sqlExpressionFactory.Like( + translatedInstance, + new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping)); + + return true; + } + + default: + // The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped, + // preventing us from translating to LIKE. + switch (methodType) + { + // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare: + // WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern + // This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice. + case StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith: + translation = + _sqlExpressionFactory.Function( + methodType is StartsEndsWithContains.StartsWith ? "left" : "right", + [ + translatedInstance, + _sqlExpressionFactory.Function( + "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. + if (translatedInstance.TypeMapping is { StoreType: not "text" }) + { + translation = _sqlExpressionFactory.Convert(translation, typeof(string), translatedInstance.TypeMapping); + } + + // We compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a simple + // equality would yield true in that case, but we want false. + translation = + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedInstance), + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedPattern), + _sqlExpressionFactory.Equal(translation, translatedPattern))); + + break; + + // For Contains, just use strpos and check if the result is greater than 0. Note that strpos returns 1 when the pattern + // is an empty string, just like .NET Contains (so no need to compensate) + case StartsEndsWithContains.Contains: + translation = + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedInstance), + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedPattern), + _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.Function( + "strpos", [translatedInstance, translatedPattern], nullable: true, + argumentsPropagateNullability: [true, true], typeof(int)), + _sqlExpressionFactory.Constant(0)))); + break; + + default: + throw new UnreachableException(); + } + + return 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. + /// + public static string? ConstructLikePatternParameter( + QueryContext queryContext, + string baseParameterName, + StartsEndsWithContains methodType) + => queryContext.Parameters[baseParameterName] switch + { + null => null, + + // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns. + // Return % which always matches instead. + "" => "%", + + string s => methodType switch + { + StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', + StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), + StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", + _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) + }, + + _ => 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 + /// 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 + } + + private static bool IsLikeWildChar(char c) + => c is '%' or '_'; + + private static string EscapeLikePattern(string pattern) + { + var builder = new StringBuilder(); + for (var i = 0; i < pattern.Length; i++) + { + var c = pattern[i]; + if (IsLikeWildChar(c) || c == LikeEscapeChar) + { + builder.Append(LikeEscapeChar); + } + + builder.Append(c); + } + + return builder.ToString(); + } + + #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) + { + if (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression) + { + var innerType = unaryExpression.Operand.Type.UnwrapNullableType(); + if (innerType.IsEnum) + { + innerType = Enum.GetUnderlyingType(innerType); + } + + var convertedType = unaryExpression.Type.UnwrapNullableType(); + + if (innerType == convertedType + || (convertedType == typeof(int) + && (innerType == typeof(byte) + || innerType == typeof(sbyte) + || innerType == typeof(char) + || innerType == typeof(short) + || innerType == typeof(ushort)))) + { + return TryRemoveImplicitConvert(unaryExpression.Operand); + } + } + + return expression; + } + + [DebuggerStepThrough] + private static bool TranslationFailed(Expression? original, Expression? translation, out SqlExpression? castTranslation) + { + if (original is not null && !(translation is SqlExpression)) + { + castTranslation = null; + return true; + } + + castTranslation = translation as SqlExpression; + return false; + } + + #endregion Copied from RelationalSqlTranslatingExpressionVisitor +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTranslatingExpressionVisitorFactory.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTranslatingExpressionVisitorFactory.cs new file mode 100644 index 0000000000..60980c8985 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTranslatingExpressionVisitorFactory.cs @@ -0,0 +1,38 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBSqlTranslatingExpressionVisitorFactory : IRelationalSqlTranslatingExpressionVisitorFactory +{ + private readonly RelationalSqlTranslatingExpressionVisitorDependencies _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 + /// 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 GaussDBSqlTranslatingExpressionVisitorFactory( + RelationalSqlTranslatingExpressionVisitorDependencies dependencies) + { + _dependencies = 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 + /// 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 RelationalSqlTranslatingExpressionVisitor Create( + QueryCompilationContext queryCompilationContext, + QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) + => new GaussDBSqlTranslatingExpressionVisitor( + _dependencies, + queryCompilationContext, + queryableMethodTranslatingExpressionVisitor); +} diff --git a/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTreePruner.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTreePruner.cs new file mode 100644 index 0000000000..262c732d11 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBSqlTreePruner.cs @@ -0,0 +1,56 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; +using ColumnInfo = HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal.GaussDBTableValuedFunctionExpression.ColumnInfo; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBSqlTreePruner : 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 GaussDBTableValuedFunctionExpression { 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.GaussDB/Query/Internal/GaussDBTypeMappingPostprocessor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBTypeMappingPostprocessor.cs new file mode 100644 index 0000000000..a0fecfd9af --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBTypeMappingPostprocessor.cs @@ -0,0 +1,62 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBTypeMappingPostprocessor : 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 GaussDBTypeMappingPostprocessor( + 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 GaussDBUnnestExpression 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.GaussDB/Query/Internal/GaussDBUnnestPostprocessor.cs b/src/EFCore.GaussDB/Query/Internal/GaussDBUnnestPostprocessor.cs new file mode 100644 index 0000000000..da7d931ad8 --- /dev/null +++ b/src/EFCore.GaussDB/Query/Internal/GaussDBUnnestPostprocessor.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Expressions.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Query.Internal; + +/// +/// Locates instances of in the tree and prunes the WITH ORDINALITY clause from them if the +/// ordinality column isn't referenced anywhere. +/// +/// +/// 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 GaussDBUnnestPostprocessor : 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. + /// + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + switch (expression) + { + case ShapedQueryExpression shapedQueryExpression: + 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); 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 GaussDBUnnestExpression unnest + && !selectExpression.Orderings.Skip(1).Select(o => o.Expression) + .Concat(selectExpression.Projection.Select(p => p.Expression)) + .Any(IsOrdinalityColumn)) + { + if (newTables is null) + { + newTables = new TableExpressionBase[selectExpression.Tables.Count]; + + for (var j = 0; j < i; j++) + { + newTables[j] = selectExpression.Tables[j]; + } + } + + var newUnnest = new GaussDBUnnestExpression(unnest.Alias, unnest.Array, unnest.ColumnName, withOrdinality: false); + + newTables[i] = table switch + { + JoinExpressionBase j => j.Update(newUnnest), + GaussDBUnnestExpression => 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( + newTables, + selectExpression.Predicate, + selectExpression.GroupBy, + selectExpression.Having, + selectExpression.Projection, + orderings, + selectExpression.Offset, + selectExpression.Limit)); + } + + default: + return base.Visit(expression); + } + } +} diff --git a/src/EFCore.GaussDB/README.md b/src/EFCore.GaussDB/README.md new file mode 100644 index 0000000000..309e844862 --- /dev/null +++ b/src/EFCore.GaussDB/README.md @@ -0,0 +1,35 @@ +# GaussDB Entity Framework Core provider for GaussDB + +HuaweiCloud.EntityFrameworkCore.GaussDB is the open source EF Core provider for GaussDB. It allows you to interact with GaussDB via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. It's built on top of [GaussDB](https://github.com/HuaweiCloudDeveloper/gaussdb-dotnet). + +The provider looks and feels just like any other Entity Framework Core provider. Here's a quick sample to get you started: + +```csharp +await using var ctx = new BlogContext(); +await ctx.Database.EnsureDeletedAsync(); +await ctx.Database.EnsureCreatedAsync(); + +// Insert a Blog +ctx.Blogs.Add(new() { Name = "FooBlog" }); +await ctx.SaveChangesAsync(); + +// Query all blogs who's name starts with F +var fBlogs = await ctx.Blogs.Where(b => b.Name.StartsWith("F")).ToListAsync(); + +public class BlogContext : DbContext +{ + public DbSet Blogs { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(@"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase"); +} + +public class Blog +{ + public int Id { get; set; } + public string Name { get; set; } +} +``` + +Aside from providing general EF Core support for GaussDB, the provider also exposes some GaussDB-specific capabilities, allowing you to query JSON, array or range columns, as well as many other advanced features. For more information, see the [the GaussDB site](https://doc.hcs.huawei.com/db/zh-cn/index.html). For information about EF Core in general, see the [EF Core website](https://docs.microsoft.com/ef/core/). + diff --git a/src/EFCore.PG/Scaffolding/Internal/DbDataReaderExtension.cs b/src/EFCore.GaussDB/Scaffolding/Internal/DbDataReaderExtension.cs similarity index 92% rename from src/EFCore.PG/Scaffolding/Internal/DbDataReaderExtension.cs rename to src/EFCore.GaussDB/Scaffolding/Internal/DbDataReaderExtension.cs index 86a41ca53a..da0a1301ed 100644 --- a/src/EFCore.PG/Scaffolding/Internal/DbDataReaderExtension.cs +++ b/src/EFCore.GaussDB/Scaffolding/Internal/DbDataReaderExtension.cs @@ -1,7 +1,7 @@ using System.Data.Common; using System.Diagnostics.CodeAnalysis; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; internal static class DbDataReaderExtension { diff --git a/src/EFCore.GaussDB/Scaffolding/Internal/GaussDBCodeGenerator.cs b/src/EFCore.GaussDB/Scaffolding/Internal/GaussDBCodeGenerator.cs new file mode 100644 index 0000000000..2693ed91fe --- /dev/null +++ b/src/EFCore.GaussDB/Scaffolding/Internal/GaussDBCodeGenerator.cs @@ -0,0 +1,40 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; + +/// +/// The default code generator for GaussDB. +/// +public class GaussDBCodeGenerator : ProviderCodeGenerator +{ + private static readonly MethodInfo _useGaussDBMethodInfo + = typeof(GaussDBDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod( + nameof(GaussDBDbContextOptionsBuilderExtensions.UseGaussDB), + typeof(DbContextOptionsBuilder), + typeof(string), + typeof(Action)); + + /// + /// Constructs an instance of the class. + /// + /// The dependencies. + public GaussDBCodeGenerator(ProviderCodeGeneratorDependencies dependencies) + : base(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 + /// 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 MethodCallCodeFragment GenerateUseProvider( + string connectionString, + MethodCallCodeFragment? providerOptions) + => new( + _useGaussDBMethodInfo, + providerOptions is null + ? [connectionString] + : [connectionString, new NestedClosureCodeFragment("x", providerOptions)]); +} diff --git a/src/EFCore.GaussDB/Scaffolding/Internal/GaussDBDatabaseModelFactory.cs b/src/EFCore.GaussDB/Scaffolding/Internal/GaussDBDatabaseModelFactory.cs new file mode 100644 index 0000000000..ae04ef4ba2 --- /dev/null +++ b/src/EFCore.GaussDB/Scaffolding/Internal/GaussDBDatabaseModelFactory.cs @@ -0,0 +1,1497 @@ +using System.Data; +using System.Data.Common; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities; +using HuaweiCloud.GaussDB; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; + +// ReSharper disable StringLiteralTypo + +/// +/// The default database model factory for GaussDB. +/// +public class GaussDBDatabaseModelFactory : DatabaseModelFactory +{ + #region Fields + + private const string NamePartRegex = """(?:(?:"(?(?:(?:"")|[^"])+)")|(?[^\.\["]+))"""; + + private static readonly Regex SchemaTableNameExtractor = + new( + string.Format( + CultureInfo.InvariantCulture, + @"^{0}(?:\.{1})?$", + string.Format(CultureInfo.InvariantCulture, NamePartRegex, 1), + string.Format(CultureInfo.InvariantCulture, NamePartRegex, 2)), + RegexOptions.Compiled, + TimeSpan.FromMilliseconds(1000.0)); + + private static readonly string[] SerialTypes = ["int2", "int4", "int8"]; + + private readonly IDiagnosticsLogger _logger; + + #endregion + + #region Public surface + + /// + /// Constructs an instance of the class. + /// + public GaussDBDatabaseModelFactory(IDiagnosticsLogger logger) + { + _logger = Check.NotNull(logger, nameof(logger)); + } + + /// + public override DatabaseModel Create(string connectionString, DatabaseModelFactoryOptions options) + { + Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotNull(options, nameof(options)); + + using var connection = new GaussDBConnection(connectionString); + return Create(connection, options); + } + + /// + public override DatabaseModel Create(DbConnection dbConnection, DatabaseModelFactoryOptions options) + { + Check.NotNull(dbConnection, nameof(dbConnection)); + Check.NotNull(options, nameof(options)); + + var databaseModel = new DatabaseModel(); + + var connection = (GaussDBConnection)dbConnection; + + var connectionStartedOpen = connection.State == ConnectionState.Open; + + if (!connectionStartedOpen) + { + connection.Open(); + } + + try + { + var internalSchemas = "'pg_catalog', 'information_schema'"; + using (var command = new GaussDBCommand("SELECT version()", connection)) + { + var longVersion = (string)command.ExecuteScalar()!; + if (longVersion.Contains("CockroachDB")) + { + internalSchemas += ", 'crdb_internal'"; + } + } + + databaseModel.DatabaseName = connection.Database; + databaseModel.DefaultSchema = "public"; + + PopulateGlobalDatabaseInfo(connection, databaseModel); + + var schemaList = options.Schemas.ToList(); + var schemaFilter = GenerateSchemaFilter(schemaList); + var tableList = options.Tables.ToList(); + var tableFilter = GenerateTableFilter(tableList.Select(Parse).ToList(), schemaFilter); + + var enums = GetEnums(connection, databaseModel); + + foreach (var table in GetTables(connection, databaseModel, tableFilter, internalSchemas, enums, _logger)) + { + table.Database = databaseModel; + databaseModel.Tables.Add(table); + } + + foreach (var table in databaseModel.Tables) + { + while (table.Columns.Remove(null!)) { } + } + + foreach (var sequence in GetSequences(connection, databaseModel, schemaFilter, _logger)) + { + sequence.Database = databaseModel; + databaseModel.Sequences.Add(sequence); + } + + if (connection.PostgreSqlVersion >= new Version(9, 1)) + { + GetExtensions(connection, databaseModel); + GetCollations(connection, databaseModel, internalSchemas, _logger); + } + + for (var i = 0; i < databaseModel.Tables.Count; i++) + { + var table = databaseModel.Tables[i]; + + // We may have dropped or skipped columns. We load these because constraints take them into + // account when referencing columns, but must now get rid of them before returning + // the database model. + while (table.Columns.Remove(null!)) { } + } + + foreach (var schema in schemaList + .Except(databaseModel.Sequences.Select(s => s.Schema).Concat(databaseModel.Tables.Select(t => t.Schema)))) + { + _logger.MissingSchemaWarning(schema); + } + + foreach (var table in tableList) + { + var (schema, name) = Parse(table); + if (!databaseModel.Tables.Any(t => !string.IsNullOrEmpty(schema) && t.Schema == schema || t.Name == name)) + { + _logger.MissingTableWarning(table); + } + } + + return databaseModel; + } + finally + { + if (!connectionStartedOpen) + { + connection.Close(); + } + } + } + + #endregion + + #region Type information queries + + private static void PopulateGlobalDatabaseInfo(GaussDBConnection connection, DatabaseModel databaseModel) + { + if (connection.PostgreSqlVersion < new Version(8, 4)) + { + return; + } + + var commandText = """ +SELECT datcollate +FROM pg_database +WHERE datname=current_database() AND datcollate <> (SELECT datcollate FROM pg_database WHERE datname='template1') +"""; + using var command = new GaussDBCommand(commandText, connection); + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + databaseModel.Collation = reader.GetString(0); + } + } + + /// + /// Queries the database for defined tables and registers them with the model. + /// + private static IEnumerable GetTables( + GaussDBConnection connection, + DatabaseModel databaseModel, + Func? tableFilter, + string internalSchemas, + HashSet enums, + IDiagnosticsLogger logger) + { + var filter = tableFilter is not null ? $"AND {tableFilter("ns.nspname", "cls.relname")}" : null; + var commandText = $""" +SELECT + nspname, relname, relkind, description, + {(connection.PostgreSqlVersion >= new Version(8, 2) ? "reloptions" : "'{}'::text[] AS reloptions")} +FROM pg_class AS cls +JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace +LEFT OUTER JOIN pg_description AS des ON des.objoid = cls.oid AND des.objsubid=0 +WHERE + cls.relkind IN ('r', 'v', 'm', 'f') AND + ns.nspname NOT IN ({internalSchemas}) AND + cls.relname <> '{HistoryRepository.DefaultTableName}' AND + -- Exclude tables which are members of PG extensions + NOT EXISTS ( + SELECT 1 FROM pg_depend WHERE + classid=( + SELECT cls.oid + FROM pg_class AS cls + JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace + WHERE relname='pg_class' AND ns.nspname='pg_catalog' + ) AND + objid=cls.oid AND + deptype IN ('e', 'x') + ) + {filter} +"""; + + var tables = new List(); + + using (var command = new GaussDBCommand(commandText, connection)) + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + var schema = reader.GetValueOrDefault("nspname"); + var name = reader.GetString("relname"); + var type = reader.GetChar("relkind"); + var comment = reader.GetValueOrDefault("description"); + var storageParameters = reader.GetValueOrDefault("reloptions") ?? []; + + var table = type switch + { + 'r' => new DatabaseTable(), + 'f' => new DatabaseTable(), + 'v' => new DatabaseView(), + 'm' => new DatabaseView(), + _ => throw new ArgumentOutOfRangeException($"Unknown relkind '{type}' when scaffolding {DisplayName(schema, name)}") + }; + + table.Database = databaseModel; + table.Name = name; + table.Schema = schema; + table.Comment = comment; + + foreach (var storageParameter in storageParameters) + { + if (storageParameter.Split("=") is [var paramName, var paramValue]) + { + table[GaussDBAnnotationNames.StorageParameterPrefix + paramName] = paramValue; + } + } + + tables.Add(table); + } + } + + GetColumns(connection, tables, filter, internalSchemas, enums, logger); + GetConstraints(connection, tables, filter, internalSchemas, out var constraintIndexes, logger); + GetIndexes(connection, tables, filter, internalSchemas, constraintIndexes, logger); + return tables; + } + + /// + /// Queries the database for defined columns and registers them with the model. + /// + private static void GetColumns( + GaussDBConnection connection, + IReadOnlyList tables, + string? tableFilter, + string internalSchemas, + HashSet enums, + IDiagnosticsLogger logger) + { + var commandText = $""" +SELECT + nspname, + cls.relname, + typ.typname, + basetyp.typname AS basetypname, + attname, + description, + {(connection.PostgreSqlVersion >= new Version(9, 1) ? "collname" : "NULL::text as collname")}, + attisdropped, + {(connection.PostgreSqlVersion >= new Version(10, 0) ? "attidentity::text" : "' '::text as attidentity")}, + {(connection.PostgreSqlVersion >= new Version(12, 0) ? "attgenerated::text" : "' '::text as attgenerated")}, + {(connection.PostgreSqlVersion >= new Version(14, 0) ? "attcompression::text" : "''::text as attcompression")}, + format_type(typ.oid, atttypmod) AS formatted_typname, + format_type(basetyp.oid, typ.typtypmod) AS formatted_basetypname, + CASE + WHEN pg_proc.proname = 'array_recv' THEN 'a' + ELSE typ.typtype + END AS typtype, + CASE WHEN pg_proc.proname='array_recv' THEN elemtyp.typname END AS elemtypname, + NOT (attnotnull OR typ.typnotnull) AS nullable, + CASE + WHEN atthasdef THEN (SELECT pg_get_expr(adbin, cls.oid) FROM pg_attrdef WHERE adrelid = cls.oid AND adnum = attr.attnum) + END AS default, + + -- Sequence options for identity columns + {(connection.PostgreSqlVersion >= new Version(10, 0) ? + "format_type(seqtypid, 0) AS seqtype, seqstart, seqmin, seqmax, seqincrement, seqcycle, seqcache" : + "NULL AS seqtype, NULL AS seqstart, NULL AS seqmin, NULL AS seqmax, NULL AS seqincrement, NULL AS seqcycle, NULL AS seqcache")} + +FROM pg_class AS cls +JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace +LEFT JOIN pg_attribute AS attr ON attrelid = cls.oid +LEFT JOIN pg_type AS typ ON attr.atttypid = typ.oid +LEFT JOIN pg_proc ON pg_proc.oid = typ.typreceive +LEFT JOIN pg_type AS elemtyp ON (elemtyp.oid = typ.typelem) +LEFT JOIN pg_type AS basetyp ON (basetyp.oid = typ.typbasetype) +LEFT JOIN pg_description AS des ON des.objoid = cls.oid AND des.objsubid = attnum +{(connection.PostgreSqlVersion >= new Version(9, 1) ? "LEFT JOIN pg_collation as coll ON coll.oid = attr.attcollation" : "")} +-- Bring in identity sequences the depend on this column +LEFT JOIN pg_depend AS dep ON dep.refobjid = cls.oid AND dep.refobjsubid = attr.attnum AND dep.deptype = 'i' +{(connection.PostgreSqlVersion >= new Version(10, 0) ? "LEFT JOIN pg_sequence AS seq ON seq.seqrelid = dep.objid" : "")} +WHERE + cls.relkind IN ('r', 'v', 'm', 'f') AND + nspname NOT IN ({internalSchemas}) AND + attnum > 0 AND + cls.relname <> '{HistoryRepository.DefaultTableName}' AND + -- Exclude tables which are members of PG extensions + NOT EXISTS ( + SELECT 1 FROM pg_depend WHERE + classid=( + SELECT cls.oid + FROM pg_class AS cls + JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace + WHERE relname='pg_class' AND ns.nspname='pg_catalog' + ) AND + objid=cls.oid AND + deptype IN ('e', 'x') + ) + {tableFilter} +ORDER BY attnum +"""; + + using var command = new GaussDBCommand(commandText, connection); + using var reader = command.ExecuteReader(); + + var tableGroups = reader.Cast().GroupBy( + ddr => ( + tableSchema: ddr.GetFieldValue("nspname"), + tableName: ddr.GetFieldValue("relname"))); + + foreach (var tableGroup in tableGroups) + { + var tableSchema = tableGroup.Key.tableSchema; + var tableName = tableGroup.Key.tableName; + + var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName); + + foreach (var record in tableGroup) + { + var columnName = record.GetFieldValue("attname"); + + // We need to know about dropped columns because constraints take them into + // account when referencing columns. We'll get rid of them before returning the model. + if (record.GetValueOrDefault("attisdropped")) + { + table.Columns.Add(null!); + continue; + } + + var formattedTypeName = AdjustFormattedTypeName(record.GetFieldValue("formatted_typname")); + var formattedBaseTypeName = record.GetValueOrDefault("formatted_basetypname"); + var (storeType, systemTypeName) = formattedBaseTypeName is null + ? (formattedTypeName, record.GetFieldValue("typname")) + : (formattedBaseTypeName, record.GetFieldValue("basetypname")); // domain type + + var column = new DatabaseColumn + { + Table = table, + Name = columnName, + StoreType = storeType, + IsNullable = record.GetValueOrDefault("nullable"), + }; + + // Enum types cannot be scaffolded for now (nor can domains of enum types), + // skip with an informative message + if (enums.Contains(formattedTypeName) || formattedBaseTypeName is not null && enums.Contains(formattedBaseTypeName)) + { + logger.EnumColumnSkippedWarning($"{DisplayName(tableSchema, tableName)}.{column.Name}"); + // We need to know about skipped columns because constraints take them into + // account when referencing columns. We'll get rid of them before returning the model. + table.Columns.Add(null!); + continue; + } + + // Default values and generated columns + var defaultValueSql = record.GetValueOrDefault("default"); + switch (record.GetFieldValue("attgenerated")) + { + case "v": + column.ComputedColumnSql = defaultValueSql; + column.IsStored = false; + break; + case "s": + column.ComputedColumnSql = defaultValueSql; + column.IsStored = true; + break; + default: + column.DefaultValueSql = defaultValueSql; + column.DefaultValue = ParseDefaultValueSql(systemTypeName, defaultValueSql); + break; + } + + // Identify IDENTITY columns, as well as SERIAL ones. + var isIdentity = false; + switch (record.GetFieldValue("attidentity")) + { + case "a": + column[GaussDBAnnotationNames.ValueGenerationStrategy] = GaussDBValueGenerationStrategy.IdentityAlwaysColumn; + isIdentity = true; + break; + case "d": + column[GaussDBAnnotationNames.ValueGenerationStrategy] = GaussDBValueGenerationStrategy.IdentityByDefaultColumn; + isIdentity = true; + break; + default: + // Hacky but necessary... + // We identify serial columns by examining their default expression, and reverse-engineer these as ValueGenerated.OnAdd. + // We can't actually parse this since the table and column names are concatenated and may contain arbitrary underscores, + // so we construct various possibilities and compare against them. + // TODO: Think about composite keys? Do serial magic only for non-composite. + if (SerialTypes.Contains(systemTypeName)) + { + var seqName = $"{column.Table.Name}_{column.Name}_seq"; + if (column.Table.Schema == "public" + && (column.DefaultValueSql == $"nextval('{seqName}'::regclass)" + || column.DefaultValueSql == $"nextval('\"{seqName}\"'::regclass)") + || // non-public schema + column.DefaultValueSql == $"nextval('{column.Table.Schema}.{seqName}'::regclass)" + || column.DefaultValueSql == $"nextval('{column.Table.Schema}.\"{seqName}\"'::regclass)" + || column.DefaultValueSql == $"nextval('\"{column.Table.Schema}\".{seqName}'::regclass)" + || column.DefaultValueSql == $"nextval('\"{column.Table.Schema}\".\"{seqName}\"'::regclass)") + { + column.DefaultValueSql = null; + // Serial is the default value generation strategy, so GaussDBAnnotationCodeGenerator + // makes sure it isn't actually rendered + column[GaussDBAnnotationNames.ValueGenerationStrategy] = GaussDBValueGenerationStrategy.SerialColumn; + } + } + + break; + } + + if (column[GaussDBAnnotationNames.ValueGenerationStrategy] is not null) + { + column.ValueGenerated = ValueGenerated.OnAdd; + } + + if (isIdentity) + { + // Get the options for the associated sequence + var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion); + var sequenceData = new IdentitySequenceOptionsData + { + StartValue = seqInfo.StartValue, + MinValue = seqInfo.MinValue, + MaxValue = seqInfo.MaxValue, + IncrementBy = (int)(seqInfo.IncrementBy ?? 1), + IsCyclic = seqInfo.IsCyclic ?? false, + NumbersToCache = seqInfo.CacheSize ?? 1 + }; + + if (!sequenceData.Equals(IdentitySequenceOptionsData.Empty)) + { + column[GaussDBAnnotationNames.IdentityOptions] = sequenceData.Serialize(); + } + } + + if (record.GetValueOrDefault("description") is { } comment) + { + column.Comment = comment; + } + + if (record.GetValueOrDefault("collname") is { } collation && collation != "default") + { + column.Collation = collation; + } + + if (record.GetValueOrDefault("attcompression") is { } compressionMethodChar) + { + column[GaussDBAnnotationNames.CompressionMethod] = compressionMethodChar switch + { + "p" => "pglz", + "l" => "lz4", + _ => null + }; + } + + logger.ColumnFound( + DisplayName(tableSchema, tableName), + column.Name, + formattedTypeName, + column.IsNullable, + isIdentity, + column.DefaultValueSql, + column.ComputedColumnSql); + + table.Columns.Add(column); + } + } + } + + private static object? ParseDefaultValueSql(string systemTypeName, string? defaultValueSql) + { + defaultValueSql = defaultValueSql?.Trim(); + + if (string.IsNullOrEmpty(defaultValueSql)) + { + return null; + } + + while (defaultValueSql.StartsWith('(') && defaultValueSql.EndsWith(')')) + { + defaultValueSql = defaultValueSql[1..^1].Trim(); + } + + return systemTypeName switch + { + "bool" or "boolean" => defaultValueSql switch + { + "true" or "yes" or "on" or "1" => true, + "false" or "no" or "off" or "0" => false, + _ => null + }, + + "smallint" or "int2" => short.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @short) ? @short : null, + "integer" or "int" or "int4" => int.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @int) ? @int : null, + "bigint" or "int8" => long.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @long) ? @long : null, + + "real" or "float4" => float.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @float) ? @float : null, + "double precision" or "float8" => double.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @double) ? @double : null, + "numeric" or "decimal" => decimal.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @decimal) ? @decimal : null, + + _ => null + }; + } + + /// + /// Queries the database for defined indexes and registers them with the model. + /// + private static void GetIndexes( + GaussDBConnection connection, + IReadOnlyList tables, + string? tableFilter, + string internalSchemas, + List constraintIndexes, + IDiagnosticsLogger logger) + { + // Load the pg_opclass table (https://www.postgresql.org/docs/current/catalog-pg-opclass.html), + // which is referenced by the indices we'll load below + var opClasses = new Dictionary(); + try + { + using var command = new GaussDBCommand("SELECT oid, opcname, opcdefault FROM pg_opclass", connection); + using var reader = command.ExecuteReader(); + + foreach (var opClass in reader.Cast()) + { + opClasses[opClass.GetFieldValue("oid")] = ( + opClass.GetFieldValue("opcname"), + opClass.GetFieldValue("opcdefault")); + } + } + catch (PostgresException e) + { + logger.Logger.LogWarning( + e, + "Could not load index operator classes from pg_opclass. Operator classes will not be scaffolded"); + } + + var collations = new Dictionary(); + + if (connection.PostgreSqlVersion >= new Version(9, 1)) + { + using (var command = new GaussDBCommand("SELECT oid, collname FROM pg_collation", connection)) + using (var reader = command.ExecuteReader()) + { + foreach (var collation in reader.Cast()) + { + collations[collation.GetFieldValue("oid")] = collation.GetFieldValue("collname"); + } + } + } + + var commandText = $""" +SELECT + idxcls.oid AS idx_oid, + nspname, + cls.relname AS cls_relname, + idxcls.relname AS idx_relname, + indisunique, + {(connection.PostgreSqlVersion >= new Version(15, 0) ? "indnullsnotdistinct" : "false AS indnullsnotdistinct")}, + {(connection.PostgreSqlVersion >= new Version(11, 0) ? "indnkeyatts" : "indnatts AS indnkeyatts")}, + {(connection.PostgreSqlVersion >= new Version(9, 6) ? "pg_indexam_has_property(am.oid, 'can_order') as amcanorder" : "amcanorder")}, + indkey, + amname, + indclass, + indoption, + {(connection.PostgreSqlVersion >= new Version(9, 1) ? "indcollation" : "''::oidvector AS indcollation")}, + {(connection.PostgreSqlVersion >= new Version(8, 2) ? "idxcls.reloptions AS idx_reloptions" : "'{}'::text[] AS idx_reloptions")}, + CASE + WHEN indexprs IS NULL THEN NULL + ELSE pg_get_expr(indexprs, cls.oid) + END AS exprs, + CASE + WHEN indpred IS NULL THEN NULL + ELSE pg_get_expr(indpred, cls.oid) + END AS pred +FROM pg_class AS cls +JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace +JOIN pg_index AS idx ON indrelid = cls.oid +JOIN pg_class AS idxcls ON idxcls.oid = indexrelid +JOIN pg_am AS am ON am.oid = idxcls.relam +WHERE + cls.relkind = 'r' AND + nspname NOT IN ({internalSchemas}) AND + NOT indisprimary AND + cls.relname <> '{HistoryRepository.DefaultTableName}' AND + -- Exclude tables which are members of PG extensions + NOT EXISTS ( + SELECT 1 FROM pg_depend WHERE + classid=( + SELECT cls.oid + FROM pg_class AS cls + JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace + WHERE relname='pg_class' AND ns.nspname='pg_catalog' + ) AND + objid=cls.oid AND + deptype IN ('e', 'x') + ) + {tableFilter} +"""; + + using (var command = new GaussDBCommand(commandText, connection)) + using (var reader = command.ExecuteReader()) + { + var tableGroups = reader.Cast().GroupBy( + ddr => ( + tableSchema: ddr.GetFieldValue("nspname"), + tableName: ddr.GetFieldValue("cls_relname"))); + + foreach (var tableGroup in tableGroups) + { + var tableSchema = tableGroup.Key.tableSchema; + var tableName = tableGroup.Key.tableName; + + var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName); + + foreach (var record in tableGroup) + { + // Constraints are detected separately (see GetConstraints), and we don't want their + // supporting indexes to appear independently. + if (constraintIndexes.Contains(record.GetFieldValue("idx_oid"))) + { + continue; + } + + var indexName = record.GetFieldValue("idx_relname"); + var index = new DatabaseIndex + { + Table = table, + Name = indexName, + IsUnique = record.GetFieldValue("indisunique") + }; + + var numKeyColumns = record.GetFieldValue("indnkeyatts"); + var columnIndices = record.GetFieldValue("indkey"); + var tableColumns = (List)table.Columns; + + if (columnIndices.Any(i => i == 0)) + { + // Expression index, not supported + logger.ExpressionIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); + continue; + + /* + var expressions = record.GetValueOrDefault("exprs"); + if (expressions is null) + throw new Exception($"Seen 0 in indkey for index {index.Name} but indexprs is null"); + index[GaussDBAnnotationNames.IndexExpression] = expressions; + */ + } + + // Key columns come before non-key (included) columns, process them first + foreach (var i in columnIndices.Take(numKeyColumns)) + { + if (tableColumns[i - 1] is { } indexKeyColumn) + { + index.Columns.Add(indexKeyColumn); + } + else + { + logger.UnsupportedColumnIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); + goto IndexEnd; + } + } + + // Now go over non-key (included columns) if any are present + if (columnIndices.Length > numKeyColumns) + { + var nonKeyColumns = new List(); + foreach (var i in columnIndices.Skip(numKeyColumns)) + { + if (tableColumns[i - 1] is { } indexKeyColumn) + { + nonKeyColumns.Add(indexKeyColumn.Name); + } + else + { + logger.UnsupportedColumnIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); + goto IndexEnd; + } + } + + // Scaffolding included/covered properties is currently blocked, see #2194 + // index[GaussDBAnnotationNames.IndexInclude] = nonKeyColumns.ToArray(); + } + + if (record.GetValueOrDefault("pred") is { } predicate) + { + index.Filter = predicate; + } + + // It's cleaner to always output the index method on the database model, + // even when it's btree (the default); + // GaussDBAnnotationCodeGenerator can then omit it as by-convention. + // However, because of https://github.com/aspnet/EntityFrameworkCore/issues/11846 we omit + // the annotation from the model entirely. + if (record.GetValueOrDefault("amname") is { } indexMethod && indexMethod != "btree") + { + index[GaussDBAnnotationNames.IndexMethod] = indexMethod; + } + + // Handle index operator classes, which we pre-loaded + var opClassNames = record + .GetFieldValue("indclass") + .Select(oid => opClasses.TryGetValue(oid, out var opc) && !opc.IsDefault ? opc.Name : null) + .ToArray(); + + if (opClassNames.Any(op => op is not null)) + { + index[GaussDBAnnotationNames.IndexOperators] = opClassNames; + } + + var columnCollations = record + .GetFieldValue("indcollation") + .Select(oid => collations.TryGetValue(oid, out var collation) && collation != "default" ? collation : null) + .ToArray(); + + if (columnCollations.Any(coll => coll is not null)) + { + index[RelationalAnnotationNames.Collation] = columnCollations; + } + + if (record.GetValueOrDefault("amcanorder")) + { + var options = record.GetFieldValue("indoption"); + + // The first bit in indoption specifies whether values are sorted in descending order, the second whether + // NULLs are sorted first instead of last. + var isDescending = options.Select(val => (val & 0x0001) != 0).ToList(); + var nullSortOrders = options + .Select(val => (val & 0x0002) != 0 ? NullSortOrder.NullsFirst : NullSortOrder.NullsLast) + .ToArray(); + + index.IsDescending = isDescending; + + if (!SortOrderHelper.IsDefaultNullSortOrder(nullSortOrders, isDescending)) + { + index[GaussDBAnnotationNames.IndexNullSortOrder] = nullSortOrders; + } + } + + if (record.GetValueOrDefault("indnullsnotdistinct")) + { + index[GaussDBAnnotationNames.NullsDistinct] = false; + } + + foreach (var storageParameter in record.GetValueOrDefault("idx_reloptions") ?? []) + { + if (storageParameter.Split("=") is [var paramName, var paramValue]) + { + index[GaussDBAnnotationNames.StorageParameterPrefix + paramName] = paramValue; + } + } + + table.Indexes.Add(index); + + IndexEnd: ; + } + } + } + } + + /// + /// Queries the database for defined constraints and registers them with the model. + /// + private static void GetConstraints( + GaussDBConnection connection, + IReadOnlyList tables, + string? tableFilter, + string internalSchemas, + out List constraintIndexes, + IDiagnosticsLogger logger) + { + var commandText = $""" +SELECT + ns.nspname, + cls.relname, + conname, + contype::text, + conkey, + conindid, + frnns.nspname AS fr_nspname, + frncls.relname AS fr_relname, + confkey, + confdeltype::text +FROM pg_class AS cls +JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace +JOIN pg_constraint as con ON con.conrelid = cls.oid +LEFT OUTER JOIN pg_class AS frncls ON frncls.oid = con.confrelid +LEFT OUTER JOIN pg_namespace as frnns ON frnns.oid = frncls.relnamespace +WHERE + cls.relkind = 'r' AND + ns.nspname NOT IN ({internalSchemas}) AND + con.contype IN ('p', 'f', 'u') AND + cls.relname <> '{HistoryRepository.DefaultTableName}' AND + -- Exclude tables which are members of PG extensions + NOT EXISTS ( + SELECT 1 FROM pg_depend WHERE + classid=( + SELECT cls.oid + FROM pg_class AS cls + JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace + WHERE relname='pg_class' AND ns.nspname='pg_catalog' + ) AND + objid=cls.oid AND + deptype IN ('e', 'x') + ) + {tableFilter} +"""; + + using var command = new GaussDBCommand(commandText, connection); + using var reader = command.ExecuteReader(); + + constraintIndexes = []; + var tableGroups = reader.Cast().GroupBy( + ddr => ( + tableSchema: ddr.GetFieldValue("nspname"), + tableName: ddr.GetFieldValue("relname"))); + + foreach (var tableGroup in tableGroups) + { + var tableSchema = tableGroup.Key.tableSchema; + var tableName = tableGroup.Key.tableName; + + var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName); + + // Primary keys + foreach (var primaryKeyRecord in tableGroup.Where(ddr => ddr.GetFieldValue("contype") == "p")) + { + var pkName = primaryKeyRecord.GetValueOrDefault("conname"); + var primaryKey = new DatabasePrimaryKey { Table = table, Name = pkName }; + + foreach (var pkColumnIndex in primaryKeyRecord.GetFieldValue("conkey")) + { + if (table.Columns[pkColumnIndex - 1] is { } pkColumn) + { + primaryKey.Columns.Add(pkColumn); + } + else + { + logger.UnsupportedColumnConstraintSkippedWarning(primaryKey.Name, DisplayName(tableSchema, tableName)); + goto PkEnd; + } + } + + table.PrimaryKey = primaryKey; + PkEnd: ; + } + + // Foreign keys + foreach (var foreignKeyRecord in tableGroup.Where(ddr => ddr.GetFieldValue("contype") == "f")) + { + var fkName = foreignKeyRecord.GetFieldValue("conname"); + var principalTableSchema = foreignKeyRecord.GetFieldValue("fr_nspname"); + var principalTableName = foreignKeyRecord.GetFieldValue("fr_relname"); + var onDeleteAction = foreignKeyRecord.GetFieldValue("confdeltype"); + + var principalTable = + tables.FirstOrDefault( + t => + principalTableSchema == t.Schema && principalTableName == t.Name) + ?? tables.FirstOrDefault( + t => + principalTableSchema.Equals(t.Schema, StringComparison.OrdinalIgnoreCase) + && principalTableName.Equals(t.Name, StringComparison.OrdinalIgnoreCase)); + + if (principalTable is null) + { + logger.ForeignKeyReferencesMissingPrincipalTableWarning( + fkName, + DisplayName(table.Schema, table.Name), + DisplayName(principalTableSchema, principalTableName)); + + continue; + } + + var foreignKey = new DatabaseForeignKey + { + Table = table, + Name = fkName, + PrincipalTable = principalTable, + OnDelete = ConvertToReferentialAction(onDeleteAction) + }; + + var columnIndices = foreignKeyRecord.GetFieldValue("conkey"); + var principalColumnIndices = foreignKeyRecord.GetFieldValue("confkey"); + + if (columnIndices.Length != principalColumnIndices.Length) + { + throw new InvalidOperationException("Found varying lengths for column and principal column indices."); + } + + var principalColumns = (List)principalTable.Columns; + + for (var i = 0; i < columnIndices.Length; i++) + { + var foreignKeyColumn = table.Columns[columnIndices[i] - 1]; + var foreignKeyPrincipalColumn = principalColumns[principalColumnIndices[i] - 1]; + if (foreignKeyColumn is null || foreignKeyPrincipalColumn is null) + { + logger.UnsupportedColumnConstraintSkippedWarning(foreignKey.Name, DisplayName(tableSchema, tableName)); + goto ForeignKeyEnd; + } + + foreignKey.Columns.Add(foreignKeyColumn); + foreignKey.PrincipalColumns.Add(foreignKeyPrincipalColumn); + } + + table.ForeignKeys.Add(foreignKey); + ForeignKeyEnd: ; + } + + // Unique constraints + foreach (var record in tableGroup.Where(ddr => ddr.GetValueOrDefault("contype") == "u")) + { + var name = record.GetValueOrDefault("conname"); + + logger.UniqueConstraintFound(name, DisplayName(tableSchema, tableName)); + + var uniqueConstraint = new DatabaseUniqueConstraint { Table = table, Name = name }; + + foreach (var columnIndex in record.GetFieldValue("conkey")) + { + var constraintColumn = table.Columns[columnIndex - 1]; + if (constraintColumn is null) + { + logger.UnsupportedColumnConstraintSkippedWarning(uniqueConstraint.Name, DisplayName(tableSchema, tableName)); + goto UniqueConstraintEnd; + } + + uniqueConstraint.Columns.Add(constraintColumn); + } + + table.UniqueConstraints.Add(uniqueConstraint); + constraintIndexes.Add(record.GetValueOrDefault("conindid")); + + UniqueConstraintEnd: ; + } + } + } + + /// + /// Queries the database for defined sequences and registers them with the model. + /// + private static IEnumerable GetSequences( + GaussDBConnection connection, + DatabaseModel databaseModel, + Func? schemaFilter, + IDiagnosticsLogger logger) + { + // 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( + GaussDBConnection 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 GaussDBCommand(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( + GaussDBConnection connection, + DatabaseModel databaseModel, + Func? schemaFilter, + IDiagnosticsLogger logger) + { + var commandText = $""" +SELECT + sequence_schema, sequence_name, + 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, + increment::bigint AS seqincrement, + 1::bigint AS seqcache, + CASE + WHEN cycle_option = 'YES' THEN TRUE + ELSE FALSE + END AS seqcycle +FROM information_schema.sequences +JOIN pg_namespace AS ns ON ns.nspname = sequence_schema +JOIN pg_class AS cls ON cls.relnamespace = ns.oid AND cls.relname = sequence_name +WHERE + cls.relkind = 'S' + /* AND seqtype IN ('integer', 'bigint', 'smallint') */ + /* Filter out owned serial and identity sequences */ + AND 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 GaussDBCommand(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"); + + 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; + } + } + } + + /// + /// Queries the database for defined enums and registers them with the model. + /// + private static HashSet GetEnums(GaussDBConnection connection, DatabaseModel databaseModel) + { + var enums = new HashSet(); + + // pg_enum doesn't exist on Redshift + if (connection.PostgreSqlVersion < new Version(8, 3)) + { + return enums; + } + + var commandText = $""" +SELECT + nspname, + typname, + array_agg(enumlabel{(connection.PostgreSqlVersion >= new Version(9, 1) ? " ORDER BY enumsortorder" : "")}) AS labels +FROM pg_enum +JOIN pg_type ON pg_type.oid = enumtypid +JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace +GROUP BY nspname, typname +"""; + + using var command = new GaussDBCommand(commandText, connection); + using var reader = command.ExecuteReader(); + + // TODO: just return a collection and make this a static utility method. + while (reader.Read()) + { + var schema = reader.GetFieldValue("nspname"); + var name = reader.GetFieldValue("typname"); + var labels = reader.GetFieldValue("labels"); + + if (schema == "public") + { + schema = null; + } + + GaussDBEnum.GetOrAddPostgresEnum(databaseModel, schema, name, labels); + enums.Add(name); + } + + return enums; + } + + /// + /// Queries the installed database extensions and registers them with the model. + /// + private static void GetExtensions(GaussDBConnection connection, DatabaseModel databaseModel) + { + const string commandText = """ +SELECT ns.nspname, extname, extversion +FROM pg_extension +JOIN pg_namespace ns ON ns.oid=extnamespace +"""; + using var command = new GaussDBCommand(commandText, connection); + using var reader = command.ExecuteReader(); + + while (reader.Read()) + { + var schema = reader.GetFieldValue("nspname"); + var name = reader.GetString(reader.GetOrdinal("extname")); + var version = reader.GetValueOrDefault("extversion"); + + if (name == "plpgsql") // Implicitly installed in all PG databases + { + continue; + } + + databaseModel.GetOrAddPostgresExtension(schema, name, version); + } + } + + private static void GetCollations( + GaussDBConnection connection, + DatabaseModel databaseModel, + string internalSchemas, + IDiagnosticsLogger logger) + { + var commandText = $""" +SELECT + nspname, collname, collprovider, collcollate, collctype, + {(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 +WHERE + nspname NOT IN ({internalSchemas}) +"""; + + try + { + using var command = new GaussDBCommand(commandText, connection); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var schema = reader.GetString(reader.GetOrdinal("nspname")); + var name = reader.GetString(reader.GetOrdinal("collname")); + var icuLocale = reader.GetValueOrDefault("colllocale"); + var lcCollate = reader.GetValueOrDefault("collcollate"); + var lcCtype = reader.GetValueOrDefault("collctype"); + var providerCode = reader.GetChar(reader.GetOrdinal("collprovider")); + var isDeterministic = reader.GetBoolean(reader.GetOrdinal("collisdeterministic")); + + string? provider; + switch (providerCode) + { + case 'c': + provider = "libc"; + break; + case 'i': + provider = "icu"; + break; + case 'd': + provider = null; + break; + default: + logger.Logger.LogWarning( + $"Unknown collation provider code {providerCode} for collation {name}, skipping."); + continue; + } + + // Starting with PG15, ICU collations only have colliculocale populated. + if (lcCollate is null || lcCtype is null) + { + Debug.Assert(lcCollate is null && lcCtype is null); + Debug.Assert(icuLocale is not null); + lcCollate = icuLocale; + lcCtype = icuLocale; + } + + logger.CollationFound(schema, name, lcCollate, lcCtype, provider, isDeterministic); + + GaussDBCollation.GetOrAddCollation( + databaseModel, schema, name, lcCollate!, lcCtype, provider, isDeterministic); + } + } + catch (PostgresException e) + { + logger.Logger.LogWarning(e, "Could not load database collations."); + } + } + + #endregion + + #region SequenceInfo + + private static SequenceInfo ReadSequenceInfo(DbDataRecord record, Version postgresVersion) + { + 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 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: + defaultMin = 1; + defaultMax = short.MaxValue; + defaultStart = minValue; + break; + + case "smallint": + // GaussDB 10 changed the default minvalue for a descending sequence, see #264 + defaultMin = postgresVersion >= new Version(10, 0) + ? short.MinValue + : short.MinValue + 1; + defaultMax = -1; + defaultStart = maxValue; + break; + + case "integer" when incrementBy > 0: + defaultMin = 1; + defaultMax = int.MaxValue; + defaultStart = minValue; + break; + + case "integer": + // GaussDB 10 changed the default minvalue for a descending sequence, see #264 + defaultMin = postgresVersion >= new Version(10, 0) + ? int.MinValue + : int.MinValue + 1; + defaultMax = -1; + defaultStart = maxValue; + break; + + case "bigint" when incrementBy > 0: + defaultMin = 1; + defaultMax = long.MaxValue; + defaultStart = minValue; + break; + + case "bigint": + // GaussDB 10 changed the default minvalue for a descending sequence, see #264 + defaultMin = postgresVersion >= new Version(10, 0) + ? long.MinValue + : long.MinValue + 1; + defaultMax = -1; + defaultStart = maxValue; + break; + + default: + throw new NotSupportedException($"Sequence has datatype {storeType} which isn't an expected sequence type."); + } + + return new SequenceInfo(storeType) + { + StartValue = startValue == defaultStart ? null : startValue, + MinValue = minValue == defaultMin ? null : minValue, + MaxValue = maxValue == defaultMax ? null : maxValue, + IncrementBy = incrementBy == 1 ? null : incrementBy, + IsCyclic = isCyclic == false ? null : true, + CacheSize = cacheSize is 1 or null ? null : cacheSize + }; + } + + private sealed class SequenceInfo(string storeType) + { + 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 int? CacheSize { get; set; } + } + + #endregion + + #region Filter fragment generators + + /// + /// Builds a delegate to generate a schema filter fragment. + /// + private static Func? GenerateSchemaFilter(IReadOnlyList schemas) + => schemas.Any() + ? s => $"{s} IN ({string.Join(", ", schemas.Select(EscapeLiteral))})" + : null; + + /// + /// Builds a delegate to generate a table filter fragment. + /// + private static Func? GenerateTableFilter( + IReadOnlyList<(string? Schema, string Table)> tables, + Func? schemaFilter) + => schemaFilter is not null || tables.Any() + ? (s, t) => + { + var tableFilterBuilder = new StringBuilder(); + + var openBracket = false; + if (schemaFilter is not null) + { + tableFilterBuilder + .Append("(") + .Append(schemaFilter(s)); + openBracket = true; + } + + if (tables.Any()) + { + if (openBracket) + { + tableFilterBuilder + .AppendLine() + .Append("OR "); + } + else + { + tableFilterBuilder.Append("("); + openBracket = true; + } + + var tablesWithoutSchema = tables.Where(e => string.IsNullOrEmpty(e.Schema)).ToList(); + if (tablesWithoutSchema.Any()) + { + tableFilterBuilder.Append(t); + tableFilterBuilder.Append(" IN ("); + tableFilterBuilder.Append(string.Join(", ", tablesWithoutSchema.Select(e => EscapeLiteral(e.Table)))); + tableFilterBuilder.Append(")"); + } + + var tablesWithSchema = tables.Where(e => !string.IsNullOrEmpty(e.Schema)).ToList(); + if (tablesWithSchema.Any()) + { + if (tablesWithoutSchema.Any()) + { + tableFilterBuilder.Append(" OR "); + } + + tableFilterBuilder.Append(t); + tableFilterBuilder.Append(" IN ("); + tableFilterBuilder.Append(string.Join(", ", tablesWithSchema.Select(e => EscapeLiteral(e.Table)))); + tableFilterBuilder.Append(") AND ("); + tableFilterBuilder.Append(s); + tableFilterBuilder.Append(" || '.' || "); + tableFilterBuilder.Append(t); + tableFilterBuilder.Append(") IN ("); + tableFilterBuilder.Append(string.Join(", ", tablesWithSchema.Select(e => EscapeLiteral($"{e.Schema}.{e.Table}")))); + tableFilterBuilder.Append(")"); + } + } + + if (openBracket) + { + tableFilterBuilder.Append(")"); + } + + return tableFilterBuilder.ToString(); + } + : null; + + #endregion + + #region Utilities + + /// + /// Type names as returned by GaussDB's format_type need to be cleaned up a bit + /// + private static string AdjustFormattedTypeName(string formattedTypeName) + { + // User-defined types (e.g. enums) with capital letters get formatted with quotes, remove. + if (formattedTypeName[0] == '"') + { + formattedTypeName = formattedTypeName.Substring(1, formattedTypeName.Length - 2); + } + + if (formattedTypeName == "bpchar") + { + formattedTypeName = "char"; + } + + return formattedTypeName; + } + + /// + /// Maps a character to a . + /// + private static ReferentialAction ConvertToReferentialAction(string onDeleteAction) + => onDeleteAction switch + { + "a" => ReferentialAction.NoAction, + "r" => ReferentialAction.Restrict, + "c" => ReferentialAction.Cascade, + "n" => ReferentialAction.SetNull, + "d" => ReferentialAction.SetDefault, + _ => throw new ArgumentOutOfRangeException( + $"Unknown value {onDeleteAction} for foreign key deletion action code.") + }; + + /// + /// Constructs the display name given a schema and table name. + /// + // TODO: should this default to/screen out the public schema? + private static string DisplayName(string? schema, string name) + => string.IsNullOrEmpty(schema) ? name : $"{schema}.{name}"; + + /// + /// Parses the table name into a tuple of schema name and table name where the schema may be null. + /// + private static (string? Schema, string Table) Parse(string table) + { + var match = SchemaTableNameExtractor.Match(table.Trim()); + + if (!match.Success) + { + throw new InvalidOperationException("The table name could not be parsed."); + } + + var part1 = match.Groups["part1"].Value; + var part2 = match.Groups["part2"].Value; + + return string.IsNullOrEmpty(part2) ? (null, part1) : (part1, part2); + } + + /// + /// Wraps a string literal in single quotes. + /// + private static string EscapeLiteral(string? s) + => $"'{s}'"; + + #endregion +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBDataSourceManager.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBDataSourceManager.cs new file mode 100644 index 0000000000..4cf16fbda8 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBDataSourceManager.cs @@ -0,0 +1,195 @@ +using System.Collections.Concurrent; +using System.Data.Common; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.GaussDB; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBDataSourceManager : 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 GaussDBDataSourceManager(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(GaussDBOptionsExtension? npgsqlOptionsExtension, IServiceProvider? applicationServiceProvider) + => npgsqlOptionsExtension switch + { + // If the user has explicitly passed in a data source via UseGaussDB(), 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 UseGaussDB(), 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(GaussDBStrings.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 UseGaussDB (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 GaussDBDataSource, since they require configuration on GaussDBDataSourceBuilder. + { 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 + // GaussDBRelationalConnection to use the connection string as before (no data source), allowing switching connection strings + // with the same service provider etc. + _ => null + }; + + private DbDataSource GetSingletonDataSource(GaussDBOptionsExtension 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(GaussDBDataSourceManager)); + } + + 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 GaussDBDataSource CreateDataSource(GaussDBOptionsExtension npgsqlOptionsExtension) + { + var dataSourceBuilder = new GaussDBDataSourceBuilder(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.GaussDB/Storage/Internal/GaussDBDatabaseCreator.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBDatabaseCreator.cs new file mode 100644 index 0000000000..699ad8d215 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBDatabaseCreator.cs @@ -0,0 +1,426 @@ +using System.Net.Sockets; +using System.Transactions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Operations; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBDatabaseCreator( + RelationalDatabaseCreatorDependencies dependencies, + IGaussDBRelationalConnection connection, + 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 + /// 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 TimeSpan RetryDelay { get; set; } = TimeSpan.FromMilliseconds(500); + + /// + /// 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 TimeSpan RetryTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// 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 Create() + { + using (var masterConnection = connection.CreateAdminConnection()) + { + try + { + Dependencies.MigrationCommandExecutor + .ExecuteNonQuery(CreateCreateOperations(), masterConnection); + } + catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_database_datname_index" }) + { + // This occurs when two connections are trying to create the same database concurrently + // (happens in the tests). Simply ignore the error. + } + + ClearPool(); + } + + Exists(); + } + + /// + /// 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 CreateAsync(CancellationToken cancellationToken = default) + { + var masterConnection = connection.CreateAdminConnection(); + await using (masterConnection.ConfigureAwait(false)) + { + try + { + await Dependencies.MigrationCommandExecutor + .ExecuteNonQueryAsync(CreateCreateOperations(), masterConnection, cancellationToken) + .ConfigureAwait(false); + } + catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_database_datname_index" }) + { + // This occurs when two connections are trying to create the same database concurrently + // (happens in the tests). Simply ignore the error. + } + + ClearPool(); + } + + await ExistsAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// 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 bool HasTables() + => Dependencies.ExecutionStrategy + .Execute( + connection, + connection => (bool)CreateHasTablesCommand() + .ExecuteScalar( + new RelationalCommandParameterObject( + connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger))!); + + /// + /// 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 Task HasTablesAsync(CancellationToken cancellationToken = default) + => Dependencies.ExecutionStrategy.ExecuteAsync( + connection, + async (connection, ct) => (bool)(await CreateHasTablesCommand() + .ExecuteScalarAsync( + new RelationalCommandParameterObject( + connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger), + cancellationToken: ct).ConfigureAwait(false))!, cancellationToken); + + private IRelationalCommand CreateHasTablesCommand() + => rawSqlCommandBuilder + .Build( + """ +SELECT CASE WHEN COUNT(*) = 0 THEN FALSE ELSE TRUE END +FROM pg_class AS cls +JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace +WHERE + cls.relkind IN ('r', 'v', 'm', 'f', 'p') AND + ns.nspname NOT IN ('pg_catalog', 'information_schema') AND + -- Exclude tables which are members of PG extensions + NOT EXISTS ( + SELECT 1 FROM pg_depend WHERE + classid=( + SELECT cls.oid + FROM pg_class AS cls + JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace + WHERE relname='pg_class' AND ns.nspname='pg_catalog' + ) AND + objid=cls.oid AND + deptype IN ('e', 'x') + ) +"""); + + private IReadOnlyList CreateCreateOperations() + { + var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; + + return Dependencies.MigrationsSqlGenerator.Generate( + [ + new GaussDBCreateDatabaseOperation + { + Name = connection.DbConnection.Database, + Template = designTimeModel.GetDatabaseTemplate(), + Collation = designTimeModel.GetCollation(), + Tablespace = designTimeModel.GetTablespace() + } + ]); + } + + /// + /// 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 bool Exists() + => Exists(async: false).GetAwaiter().GetResult(); + + /// + /// 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 Task ExistsAsync(CancellationToken cancellationToken = default) + => Exists(async: true, cancellationToken); + + 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 - GaussDBConnectionStringBuilder.Validate will throw, + // so we turn off Multiplexing as well. + var unpooledCsb = new GaussDBConnectionStringBuilder(connection.ConnectionString) { Pooling = false, Multiplexing = false }; + + using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + var unpooledRelationalConnection = + await connection.CloneWith(unpooledCsb.ToString(), async, cancellationToken).ConfigureAwait(false); + + try + { + if (async) + { + await unpooledRelationalConnection.OpenAsync(errorsExpected: true, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + else + { + unpooledRelationalConnection.Open(errorsExpected: true); + } + + return true; + } + catch (PostgresException e) + { + if (IsDoesNotExist(e)) + { + return false; + } + + throw; + } + catch (GaussDBException e) when ( + // This can happen when GaussDB attempts to connect to multiple hosts + e.InnerException is AggregateException ae && ae.InnerExceptions.Any(ie => ie is PostgresException pe && IsDoesNotExist(pe))) + { + return false; + } + catch (GaussDBException e) when ( + e.InnerException is IOException { InnerException: SocketException { SocketErrorCode: SocketError.ConnectionReset } }) + { + // Pretty awful hack around #104 + return false; + } + finally + { + if (async) + { + await unpooledRelationalConnection.CloseAsync().ConfigureAwait(false); + await unpooledRelationalConnection.DisposeAsync().ConfigureAwait(false); + } + else + { + unpooledRelationalConnection.Close(); + unpooledRelationalConnection.Dispose(); + } + } + } + + // Login failed is thrown when database does not exist (See Issue #776) + private static bool IsDoesNotExist(PostgresException exception) + => exception.SqlState == "3D000"; + + /// + /// 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 Delete() + { + switch (connection.DataSource) + { + case GaussDBDataSource dataSource: + dataSource.Clear(); + break; + case null: + ClearAllPools(); + break; + } + + using (var masterConnection = connection.CreateAdminConnection()) + { + Dependencies.MigrationCommandExecutor + .ExecuteNonQuery(CreateDropCommands(), masterConnection); + } + } + + /// + /// 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 DeleteAsync(CancellationToken cancellationToken = default) + { + switch (connection.DataSource) + { + case GaussDBDataSource 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(); + await using (masterConnection) + { + await Dependencies.MigrationCommandExecutor + .ExecuteNonQueryAsync(CreateDropCommands(), masterConnection, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + /// 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 CreateTables() + { + var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; + var operations = Dependencies.ModelDiffer.GetDifferences(null, designTimeModel.GetRelationalModel()); + var commands = Dependencies.MigrationsSqlGenerator.Generate(operations, designTimeModel); + + // If a GaussDB extension, enum or range was added, we want GaussDB to reload all types at the ADO.NET level. + var reloadTypes = + operations.OfType() + .Any( + o => + o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); + + try + { + Dependencies.MigrationCommandExecutor.ExecuteNonQuery(commands, connection); + } + catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_type_typname_nsp_index" }) + { + // This occurs when two connections are trying to create the same database concurrently + // (happens in the tests). Simply ignore the error. + } + + if (reloadTypes && connection.DbConnection is GaussDBConnection npgsqlConnection) + { + npgsqlConnection.Open(); + try + { + npgsqlConnection.ReloadTypes(); + } + finally + { + npgsqlConnection.Close(); + } + } + } + + /// + /// 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 CreateTablesAsync(CancellationToken cancellationToken = default) + { + var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; + var operations = Dependencies.ModelDiffer.GetDifferences(null, designTimeModel.GetRelationalModel()); + var commands = Dependencies.MigrationsSqlGenerator.Generate(operations, designTimeModel); + + try + { + await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(commands, connection, cancellationToken) + .ConfigureAwait(false); + } + catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_type_typname_nsp_index" }) + { + // This occurs when two connections are trying to create the same database concurrently + // (happens in the tests). Simply ignore the error. + } + + // If a GaussDB extension, enum or range was added, we want GaussDB to reload all types at the ADO.NET level. + var reloadTypes = operations + .OfType() + .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); + + if (reloadTypes && connection.DbConnection is GaussDBConnection npgsqlConnection) + { + await npgsqlConnection.OpenAsync(cancellationToken).ConfigureAwait(false); + try + { + await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + await npgsqlConnection.CloseAsync().ConfigureAwait(false); + } + } + } + + private IReadOnlyList CreateDropCommands() + { + var operations = new MigrationOperation[] + { + // TODO Check DbConnection.Database always gives us what we want + // Issue #775 + new GaussDBDropDatabaseOperation { Name = connection.DbConnection.Database } + }; + + return Dependencies.MigrationsSqlGenerator.Generate(operations); + } + + // Clear connection pools in case there are active connections that are pooled + private static void ClearAllPools() + => GaussDBConnection.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() + => GaussDBConnection.ClearPool((GaussDBConnection)connection.DbConnection); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBExecutionStrategy.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBExecutionStrategy.cs new file mode 100644 index 0000000000..6dc721ddb6 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBExecutionStrategy.cs @@ -0,0 +1,75 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBExecutionStrategy : IExecutionStrategy +{ + private ExecutionStrategyDependencies Dependencies { 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 GaussDBExecutionStrategy(ExecutionStrategyDependencies dependencies) + { + Dependencies = 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 + /// 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 bool RetriesOnFailure + => false; + + /// + /// 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 TResult Execute( + TState state, + Func operation, + Func>? verifySucceeded) + { + try + { + return operation(Dependencies.CurrentContext.Context, state); + } + catch (Exception ex) when (ExecutionStrategy.CallOnWrappedException(ex, GaussDBTransientExceptionDetector.ShouldRetryOn)) + { + throw new InvalidOperationException("An exception has been raised that is likely due to a transient failure.", ex); + } + } + + /// + /// 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 async Task ExecuteAsync( + TState state, + Func> operation, + Func>>? verifySucceeded, + CancellationToken cancellationToken) + { + try + { + return await operation(Dependencies.CurrentContext.Context, state, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ExecutionStrategy.CallOnWrappedException(ex, GaussDBTransientExceptionDetector.ShouldRetryOn)) + { + throw new InvalidOperationException("An exception has been raised that is likely due to a transient failure.", ex); + } + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBExecutionStrategyFactory.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBExecutionStrategyFactory.cs new file mode 100644 index 0000000000..117c24ea16 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBExecutionStrategyFactory.cs @@ -0,0 +1,31 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBExecutionStrategyFactory : RelationalExecutionStrategyFactory +{ + /// + /// 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 GaussDBExecutionStrategyFactory( + ExecutionStrategyDependencies dependencies) + : base(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 + /// 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 IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) + => new GaussDBExecutionStrategy(dependencies); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBRelationalConnection.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBRelationalConnection.cs new file mode 100644 index 0000000000..c6809a81c2 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBRelationalConnection.cs @@ -0,0 +1,254 @@ +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Net.Security; +using System.Transactions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.GaussDB; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBRelationalConnection : RelationalConnection, IGaussDBRelationalConnection +{ + private readonly ProvideClientCertificatesCallback? _provideClientCertificatesCallback; + private readonly RemoteCertificateValidationCallback? _remoteCertificateValidationCallback; + +#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete + private readonly ProvidePasswordCallback? _providePasswordCallback; +#pragma warning restore CS0618 + + /// + /// 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 + /// + protected override bool SupportsAmbientTransactions + => 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. + /// + public GaussDBRelationalConnection( + RelationalConnectionDependencies dependencies, + GaussDBDataSourceManager dataSourceManager, + IDbContextOptions options) + : this( + dependencies, + dataSourceManager.GetDataSource( + options.FindExtension(), + options.FindExtension()?.ApplicationServiceProvider)) + { + } + + /// + /// 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 GaussDBRelationalConnection(RelationalConnectionDependencies dependencies, DbDataSource? dataSource) + : base(dependencies) + { + if (dataSource is not null) + { + DataSource = dataSource; + +#if DEBUG + // We validate in GaussDBOptionsExtensions.Validate that DataSource and these callbacks aren't specified together + if (dependencies.ContextOptions.FindExtension() is { } gaussdbOptions) + { + Check.DebugAssert( + gaussdbOptions?.ProvideClientCertificatesCallback is null, + "Both DataSource and ProvideClientCertificatesCallback are non-null"); + Check.DebugAssert( + gaussdbOptions?.RemoteCertificateValidationCallback is null, + "Both DataSource and RemoteCertificateValidationCallback are non-null"); + Check.DebugAssert( + gaussdbOptions?.ProvidePasswordCallback is null, + "Both DataSource and ProvidePasswordCallback are non-null"); + } +#endif + } + else if (dependencies.ContextOptions.FindExtension() is { } gaussdbOptions) + { + _provideClientCertificatesCallback = gaussdbOptions.ProvideClientCertificatesCallback; + _remoteCertificateValidationCallback = gaussdbOptions.RemoteCertificateValidationCallback; + _providePasswordCallback = gaussdbOptions.ProvidePasswordCallback; + } + } + + /// + /// 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 DbConnection CreateDbConnection() + { + if (DataSource is not null) + { + return DataSource.CreateConnection(); + } + + var conn = new GaussDBConnection(ConnectionString); + + if (_provideClientCertificatesCallback is not null || _remoteCertificateValidationCallback is not null) + { + conn.SslClientAuthenticationOptionsCallback = o => + { + if (_provideClientCertificatesCallback is not null) + { + o.ClientCertificates ??= new(); + _provideClientCertificatesCallback(o.ClientCertificates); + } + + o.RemoteCertificateValidationCallback = _remoteCertificateValidationCallback; + }; + } + + if (_providePasswordCallback is not null) + { +#pragma warning disable 618 // ProvidePasswordCallback is obsolete + conn.ProvidePasswordCallback = _providePasswordCallback; +#pragma warning restore 618 + } + + return conn; + } + + /// + /// 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. + /// + // 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; + set + { + base.ConnectionString = value; + + DataSource = 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. + /// + [AllowNull] + public new virtual GaussDBConnection DbConnection + { + get => (GaussDBConnection)base.DbConnection; + set + { + base.DbConnection = value; + + DataSource = 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 virtual DbDataSource? DbDataSource + { + get => DataSource; + set + { + DbConnection = null; + ConnectionString = null; + DataSource = value; + } + } + + /// + /// 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 IGaussDBRelationalConnection CreateAdminConnection() + { + if (Dependencies.ContextOptions.FindExtension() is not { } gaussdbOptions) + { + throw new InvalidOperationException($"{nameof(GaussDBOptionsExtension)} not found in {nameof(CreateAdminConnection)}"); + } + + var adminConnectionString = new GaussDBConnectionStringBuilder(ConnectionString) + { + Database = gaussdbOptions.AdminDatabase ?? "postgres", + Pooling = false, + Multiplexing = false + }.ToString(); + + var adminGaussDBOptions = DataSource is not null + ? gaussdbOptions.WithConnection(((GaussDBConnection)CreateDbConnection()).CloneWith(adminConnectionString)) + : gaussdbOptions.Connection is not null + ? gaussdbOptions.WithConnection(DbConnection.CloneWith(adminConnectionString)) + : gaussdbOptions.WithConnectionString(adminConnectionString); + + var optionsBuilder = new DbContextOptionsBuilder(); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(adminGaussDBOptions); + + return new GaussDBRelationalConnection(Dependencies with { ContextOptions = optionsBuilder.Options }, dataSource: 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. + /// + // Accessing Transaction.Current is expensive, so don't do it if Enlist is false in the connection string + public override Transaction? CurrentAmbientTransaction + => ConnectionString is null || !ConnectionString.Contains("Enlist=false", StringComparison.InvariantCultureIgnoreCase) + ? Transaction.Current + : 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 virtual async ValueTask CloneWith( + string connectionString, + bool async, + CancellationToken cancellationToken = default) + { + var clonedDbConnection = async + ? await DbConnection.CloneWithAsync(connectionString, cancellationToken).ConfigureAwait(false) + : DbConnection.CloneWith(connectionString); + + var relationalOptions = RelationalOptionsExtension.Extract(Dependencies.ContextOptions) + .WithConnectionString(null) + .WithConnection(clonedDbConnection); + + var optionsBuilder = new DbContextOptionsBuilder(); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(relationalOptions); + + return new GaussDBRelationalConnection(Dependencies with { ContextOptions = optionsBuilder.Options }, dataSource: null); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBSqlGenerationHelper.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBSqlGenerationHelper.cs new file mode 100644 index 0000000000..8ad7ff33fa --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBSqlGenerationHelper.cs @@ -0,0 +1,113 @@ +using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBSqlGenerationHelper : RelationalSqlGenerationHelper +{ + private static readonly HashSet ReservedWords; + + static GaussDBSqlGenerationHelper() + { + // https://www.postgresql.org/docs/current/static/sql-keywords-appendix.html + using (var conn = new GaussDBConnection()) + { + ReservedWords = + [..conn.GetSchema("ReservedWords").Rows.Cast().Select(r => (string)r["ReservedWord"])]; + } + } + + /// + /// 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 GaussDBSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies) + : base(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 + /// 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 string DelimitIdentifier(string identifier) + => RequiresQuoting(identifier) ? base.DelimitIdentifier(identifier) : identifier; + + /// + /// 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 DelimitIdentifier(StringBuilder builder, string identifier) + { + if (RequiresQuoting(identifier)) + { + base.DelimitIdentifier(builder, identifier); + } + else + { + builder.Append(identifier); + } + } + + /// + /// Returns whether the given string can be used as an unquoted identifier in GaussDB, without quotes. + /// + private static bool RequiresQuoting(string identifier) + { + var first = identifier[0]; + if (!char.IsLower(first) && first != '_') + { + return true; + } + + for (var i = 1; i < identifier.Length; i++) + { + var c = identifier[i]; + + if (char.IsLower(c)) + { + continue; + } + + switch (c) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '_': + case '$': // yes it's true + continue; + } + + return true; + } + + if (ReservedWords.Contains(identifier.ToUpperInvariant())) + { + return true; + } + + return false; + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBTransientExceptionDetector.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBTransientExceptionDetector.cs new file mode 100644 index 0000000000..6624db391d --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBTransientExceptionDetector.cs @@ -0,0 +1,19 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBTransientExceptionDetector +{ + /// + /// 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 ShouldRetryOn(Exception? ex) + => (ex as GaussDBException)?.IsTransient == true || ex is TimeoutException; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/GaussDBTypeMappingSource.cs b/src/EFCore.GaussDB/Storage/Internal/GaussDBTypeMappingSource.cs new file mode 100644 index 0000000000..bad392381b --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/GaussDBTypeMappingSource.cs @@ -0,0 +1,1227 @@ +using System.Collections; +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; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 GaussDBTypeMappingSource : RelationalTypeMappingSource +{ +#if DEBUG + internal static bool LegacyTimestampBehavior; + + // ReSharper disable once FieldCanBeMadeReadOnly.Global + internal static bool DisableDateTimeInfinityConversions; +#else + internal static readonly bool LegacyTimestampBehavior; + internal static readonly bool DisableDateTimeInfinityConversions; +#endif + + static GaussDBTypeMappingSource() + { + LegacyTimestampBehavior = AppContext.TryGetSwitch("GaussDB.EnableLegacyTimestampBehavior", out var enabled) && enabled; + DisableDateTimeInfinityConversions = AppContext.TryGetSwitch("GaussDB.DisableDateTimeInfinityConversions", out enabled) && enabled; + } + + private readonly ISqlGenerationHelper _sqlGenerationHelper; + + /// + /// 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 ConcurrentDictionary StoreTypeMappings { 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. + /// + protected virtual ConcurrentDictionary ClrTypeMappings { get; } + + private readonly IReadOnlyList _enumDefinitions; + private readonly IReadOnlyList _userRangeDefinitions; + + private readonly bool _supportsMultiranges; + + #region Mappings + + // Numeric types + private readonly GaussDBFloatTypeMapping _float4 = GaussDBFloatTypeMapping.Default; + private readonly GaussDBDoubleTypeMapping _float8 = GaussDBDoubleTypeMapping.Default; + private readonly GaussDBDecimalTypeMapping _numeric = GaussDBDecimalTypeMapping.Default; + private readonly GaussDBBigIntegerTypeMapping _bigInteger = GaussDBBigIntegerTypeMapping.Default; + private readonly GaussDBDecimalTypeMapping _numericAsFloat = new(typeof(float)); + private readonly GaussDBDecimalTypeMapping _numericAsDouble = new(typeof(double)); + private readonly GaussDBMoneyTypeMapping _money = GaussDBMoneyTypeMapping.Default; + private readonly GuidTypeMapping _uuid = new("uuid", DbType.Guid); + private readonly ShortTypeMapping _int2 = new("smallint", DbType.Int16); + private readonly ByteTypeMapping _int2Byte = new("smallint", DbType.Byte); + private readonly IntTypeMapping _int4 = new("integer", DbType.Int32); + private readonly LongTypeMapping _int8 = new("bigint", DbType.Int64); + + // Character types + private readonly StringTypeMapping _text = new("text", DbType.String); + private readonly GaussDBStringTypeMapping _varchar = new("character varying", GaussDBDbType.Varchar); + private readonly GaussDBCharacterStringTypeMapping _char = new("character"); + private readonly GaussDBCharacterCharTypeMapping _singleChar = new("character(1)"); + private readonly GaussDBStringTypeMapping _xml = new("xml", GaussDBDbType.Xml); + private readonly GaussDBStringTypeMapping _citext = new("citext", GaussDBDbType.Citext); + private readonly GaussDBStringTypeMapping _jsonpath = new("jsonpath", GaussDBDbType.JsonPath); + + // JSON mappings - EF owned entity support + private readonly GaussDBOwnedJsonTypeMapping _jsonbOwned = new("jsonb"); + private readonly GaussDBOwnedJsonTypeMapping _jsonOwned = new("json"); + + // JSON mappings - older string/weakly-typed support + private readonly GaussDBJsonTypeMapping _jsonbString = new("jsonb", typeof(string)); + private readonly GaussDBJsonTypeMapping _jsonString = new("json", typeof(string)); + private readonly GaussDBJsonTypeMapping _jsonbDocument = new("jsonb", typeof(JsonDocument)); + private readonly GaussDBJsonTypeMapping _jsonDocument = new("json", typeof(JsonDocument)); + private readonly GaussDBJsonTypeMapping _jsonbElement = new("jsonb", typeof(JsonElement)); + private readonly GaussDBJsonTypeMapping _jsonElement = new("json", typeof(JsonElement)); + + // Date/Time types + private readonly GaussDBDateTimeDateTypeMapping _dateDateTime = GaussDBDateTimeDateTypeMapping.Default; + private readonly GaussDBTimestampTypeMapping _timestamp = GaussDBTimestampTypeMapping.Default; + private readonly GaussDBTimestampTzTypeMapping _timestamptz = GaussDBTimestampTzTypeMapping.Default; + private readonly GaussDBTimestampTzTypeMapping _timestamptzDto = new(typeof(DateTimeOffset)); + private readonly GaussDBIntervalTypeMapping _interval = GaussDBIntervalTypeMapping.Default; + private readonly GaussDBTimeTypeMapping _timeTimeSpan = new(typeof(TimeSpan)); + private readonly GaussDBTimeTzTypeMapping _timetz = GaussDBTimeTzTypeMapping.Default; + + private readonly GaussDBDateOnlyTypeMapping _dateDateOnly = GaussDBDateOnlyTypeMapping.Default; + private readonly GaussDBTimeTypeMapping _timeTimeOnly = GaussDBTimeTypeMapping.Default; + + // Network address types + private readonly GaussDBMacaddrTypeMapping _macaddr = GaussDBMacaddrTypeMapping.Default; + private readonly GaussDBMacaddr8TypeMapping _macaddr8 = GaussDBMacaddr8TypeMapping.Default; + private readonly GaussDBInetTypeMapping _inetAsIPAddress = GaussDBInetTypeMapping.Default; + private readonly GaussDBInetTypeMapping _inetAsGaussDBInet = new(typeof(GaussDBInet)); + [Obsolete] + private readonly GaussDBCidrTypeMapping _cidr = GaussDBCidrTypeMapping.Default; + + // Built-in geometric types + private readonly GaussDBPointTypeMapping _point = GaussDBPointTypeMapping.Default; + private readonly GaussDBBoxTypeMapping _box = GaussDBBoxTypeMapping.Default; + private readonly GaussDBLineTypeMapping _line = GaussDBLineTypeMapping.Default; + private readonly GaussDBLineSegmentTypeMapping _lseg = GaussDBLineSegmentTypeMapping.Default; + private readonly GaussDBPathTypeMapping _path = GaussDBPathTypeMapping.Default; + private readonly GaussDBPolygonTypeMapping _polygon = GaussDBPolygonTypeMapping.Default; + private readonly GaussDBCircleTypeMapping _circle = GaussDBCircleTypeMapping.Default; + + // uint/ulong mappings + private readonly GaussDBUIntTypeMapping _xid = new("xid", GaussDBDbType.Xid); + private readonly GaussDBULongTypeMapping _xid8 = new("xid8", GaussDBDbType.Xid8); + private readonly GaussDBUIntTypeMapping _oid = new("oid", GaussDBDbType.Oid); + private readonly GaussDBUIntTypeMapping _cid = new("cid", GaussDBDbType.Cid); + private readonly GaussDBUIntTypeMapping _regtype = new("regtype", GaussDBDbType.Regtype); + private readonly GaussDBUIntTypeMapping _lo = new("lo", GaussDBDbType.Oid); + + // Full text search mappings + private readonly GaussDBTsQueryTypeMapping _tsquery = GaussDBTsQueryTypeMapping.Default; + private readonly GaussDBTsVectorTypeMapping _tsvector = GaussDBTsVectorTypeMapping.Default; + private readonly GaussDBRegconfigTypeMapping _regconfig = GaussDBRegconfigTypeMapping.Default; + private readonly GaussDBTsRankingNormalizationTypeMapping _rankingNormalization = GaussDBTsRankingNormalizationTypeMapping.Default; + + // Unaccent mapping + private readonly GaussDBRegdictionaryTypeMapping _regdictionary = new(); + + // Built-in ranges + // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable + private readonly GaussDBRangeTypeMapping _int4range; + private readonly GaussDBRangeTypeMapping _int8range; + private readonly GaussDBRangeTypeMapping _numrange; + private readonly GaussDBRangeTypeMapping _tsrange; + private readonly GaussDBRangeTypeMapping _tstzrange, _tstzrangeDto; + private readonly GaussDBRangeTypeMapping _dateOnlyDaterange; + private readonly GaussDBRangeTypeMapping _dateTimeDaterange; + + // Other types + private readonly GaussDBBoolTypeMapping _bool = GaussDBBoolTypeMapping.Default; + private readonly GaussDBBitTypeMapping _bit = GaussDBBitTypeMapping.Default; + private readonly GaussDBVarbitTypeMapping _varbit = GaussDBVarbitTypeMapping.Default; + private readonly GaussDBByteArrayTypeMapping _bytea = GaussDBByteArrayTypeMapping.Default; + private readonly GaussDBHstoreTypeMapping _hstore = GaussDBHstoreTypeMapping.Default; + private readonly GaussDBHstoreTypeMapping _immutableHstore = new(typeof(ImmutableDictionary)); + private readonly GaussDBTidTypeMapping _tid = GaussDBTidTypeMapping.Default; + private readonly GaussDBPgLsnTypeMapping _pgLsn = GaussDBPgLsnTypeMapping.Default; + + private readonly GaussDBLTreeTypeMapping _ltree = GaussDBLTreeTypeMapping.Default; + private readonly GaussDBStringTypeMapping _ltreeString = new("ltree", GaussDBDbType.LTree); + private readonly GaussDBStringTypeMapping _lquery = new("lquery", GaussDBDbType.LQuery); + private readonly GaussDBStringTypeMapping _ltxtquery = new("ltxtquery", GaussDBDbType.LTxtQuery); + + /// + /// 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. + /// + // Special stuff + // ReSharper disable once InconsistentNaming + public readonly GaussDBEStringTypeMapping EStringTypeMapping = GaussDBEStringTypeMapping.Default; + + #endregion Mappings + + /// + /// 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] + public GaussDBTypeMappingSource( + TypeMappingSourceDependencies dependencies, + RelationalTypeMappingSourceDependencies relationalDependencies, + ISqlGenerationHelper sqlGenerationHelper, + IGaussDBSingletonOptions options) + : base(dependencies, relationalDependencies) + { + _supportsMultiranges = options.PostgresVersion.AtLeast(14); + _sqlGenerationHelper = Check.NotNull(sqlGenerationHelper, nameof(sqlGenerationHelper)); + + // Initialize range mappings, which reference on other mappings + _int4range = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "int4range", typeof(GaussDBRange), GaussDBDbType.IntegerRange, _int4); + _int8range = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "int8range", typeof(GaussDBRange), GaussDBDbType.BigIntRange, _int8); + _numrange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "numrange", typeof(GaussDBRange), GaussDBDbType.NumericRange, _numeric); + _tsrange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tsrange", typeof(GaussDBRange), GaussDBDbType.TimestampRange, _timestamp); + _tstzrange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tstzrange", typeof(GaussDBRange), GaussDBDbType.TimestampTzRange, _timestamptz); + _tstzrangeDto = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "tstzrange", typeof(GaussDBRange), GaussDBDbType.TimestampTzRange, _timestamptzDto); + _dateOnlyDaterange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "daterange", typeof(GaussDBRange), GaussDBDbType.DateRange, _dateDateOnly); + _dateTimeDaterange = GaussDBRangeTypeMapping.CreatBuiltInRangeMapping( + "daterange", typeof(GaussDBRange), GaussDBDbType.DateRange, _dateDateTime); + + // ReSharper disable CoVariantArrayConversion + // Note that PostgreSQL has aliases to some built-in type name aliases (e.g. int4 for integer), + // these are mapped as well. + // https://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE + var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "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", [_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, _inetAsGaussDBInet] }, + { "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 + + // Set up aliases + storeTypeMappings["timestamp"] = storeTypeMappings["timestamp without time zone"]; + storeTypeMappings["timestamptz"] = storeTypeMappings["timestamp with time zone"]; + storeTypeMappings["time"] = storeTypeMappings["time without time zone"]; + storeTypeMappings["timetz"] = storeTypeMappings["time with time zone"]; + + var clrTypeMappings = new Dictionary + { + { typeof(bool), _bool }, + { typeof(Guid), _uuid }, + { typeof(byte), _int2Byte }, + { typeof(short), _int2 }, + { typeof(int), _int4 }, + { typeof(long), _int8 }, + { typeof(float), _float4 }, + { typeof(double), _float8 }, + { typeof(decimal), _numeric }, + { typeof(BigInteger), _bigInteger }, + { typeof(string), _text }, + { typeof(JsonDocument), _jsonbDocument }, + { typeof(JsonElement), _jsonbElement }, + { typeof(JsonTypePlaceholder), _jsonbOwned }, + { typeof(char), _singleChar }, + { typeof(DateTime), LegacyTimestampBehavior ? _timestamp : _timestamptz }, + { typeof(DateOnly), _dateDateOnly }, + { typeof(TimeOnly), _timeTimeOnly }, + { typeof(TimeSpan), _interval }, + { typeof(DateTimeOffset), _timestamptzDto }, + { typeof(PhysicalAddress), _macaddr }, + { typeof(IPAddress), _inetAsIPAddress }, + { typeof(GaussDBInet), _inetAsGaussDBInet }, + { typeof(Metadata.GaussDBRange), _cidr }, + { typeof(BitArray), _varbit }, + { typeof(ImmutableDictionary), _immutableHstore }, + { typeof(Dictionary), _hstore }, + { typeof(GaussDBTid), _tid }, + { typeof(GaussDBLogSequenceNumber), _pgLsn }, + { typeof(GaussDBPoint), _point }, + { typeof(GaussDBBox), _box }, + { typeof(GaussDBLine), _line }, + { typeof(GaussDBLSeg), _lseg }, + { typeof(GaussDBPath), _path }, + { typeof(GaussDBPolygon), _polygon }, + { typeof(GaussDBCircle), _circle }, + { typeof(GaussDBRange), _int4range }, + { typeof(GaussDBRange), _int8range }, + { typeof(GaussDBRange), _numrange }, + { typeof(GaussDBRange), LegacyTimestampBehavior ? _tsrange : _tstzrange }, + { typeof(GaussDBRange), _tstzrange }, + { typeof(GaussDBRange), _dateOnlyDaterange }, + { typeof(GaussDBTsQuery), _tsquery }, + { typeof(GaussDBTsVector), _tsvector }, + { typeof(GaussDBTsRankingNormalization), _rankingNormalization }, + { typeof(LTree), _ltree } + }; + + StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); + ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); + + _enumDefinitions = options.EnumDefinitions; + _userRangeDefinitions = options.UserRangeDefinitions; + } + + /// + /// 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 RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) + // 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); + + /// + /// 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? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo) + { + var clrType = mappingInfo.ClrType; + var storeTypeName = mappingInfo.StoreTypeName; + var storeTypeNameBase = mappingInfo.StoreTypeNameBase; + + if (storeTypeName is not null) + { + if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings)) + { + // We found the user-specified store type. No CLR type was provided - we're probably + // scaffolding from an existing database, take the first mapping as the default. + if (clrType is null) + { + return mappings[0]; + } + + // A CLR type was provided - look for a mapping between the store and CLR types. If not found, fail + // immediately. + foreach (var m in mappings) + { + if (m.ClrType == clrType) + { + return m; + } + } + + // Map arbitrary user POCOs to JSON + if (storeTypeName is "jsonb" or "json") + { + return new GaussDBJsonTypeMapping(storeTypeName, clrType); + } + + return null; + } + + if (StoreTypeMappings.TryGetValue(storeTypeNameBase!, out mappings)) + { + if (clrType is null) + { + return mappings[0]; + } + + foreach (var m in mappings) + { + if (m.ClrType == clrType) + { + return m; + } + } + + return null; + } + + // 'character' is special: 'character' (no size) and 'character(1)' map to a single char, whereas 'character(n)' maps + // to a string + if (storeTypeNameBase is "character" or "char") + { + if (mappingInfo.Size is null or 1 && clrType is null || clrType == typeof(char)) + { + return _singleChar.Clone(mappingInfo); + } + + if (clrType is null || clrType == typeof(string)) + { + return _char.Clone(mappingInfo); + } + } + + 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 + RelationalTypeMapping collectionMapping) + { + return collectionMapping; + } + + // A store type name was provided, but is unknown. This could be a domain (alias) type, in which case + // we proceed with a CLR type lookup (if the type doesn't exist at all the failure will come later). + } + + if (clrType is not null) + { + if (ClrTypeMappings.TryGetValue(clrType, out var mapping)) + { + // Handle types with the size facet (string, bitarray) + if (mappingInfo.Size > 0) + { + if (clrType == typeof(string)) + { + mapping = mappingInfo.IsFixedLength ?? false ? _char : _varchar; + + // See #342 for when size > 10485760 + return mappingInfo.Size <= 10485760 + ? mapping.WithStoreTypeAndSize($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size) + : _text; + } + + if (clrType == typeof(BitArray)) + { + mapping = mappingInfo.IsFixedLength ?? false ? _bit : _varbit; + return mapping.WithStoreTypeAndSize($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size); + } + } + + return mapping; + } + + if (clrType == typeof(byte[]) && mappingInfo.ElementTypeMapping is null) + { + if (storeTypeName == "smallint[]") + { + // PostgreSQL has no tinyint (single-byte) type, but we allow mapping CLR byte to PG smallint (2-bytes). + // The same applies to arrays - as always - so byte[] should be mappable to smallint[]. + // However, byte[] also has a base mapping to bytea, which is the default. So when the user explicitly specified + // mapping to smallint[], we don't return that to allow the array mapping to work. + // TODO: This is a workaround; RelationalTypeMappingSource first attempts to find a value converter before trying + // to find a collection. We should reverse the order and call FindCollectionMapping before attempting to find a + // value converter. + // TODO: Make sure the providerType should be null + return FindCollectionMapping(mappingInfo, typeof(byte[]), providerType: null, elementMapping: null); + // return null; + } + + return _bytea; + } + + if (mappingInfo.IsRowVersion == true) + { + if (clrType == typeof(uint)) + { + return _xid; + } + + if (clrType == typeof(ulong)) + { + return _xid8; + } + } + } + + return 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 override RelationalTypeMapping? FindCollectionMapping( + RelationalTypeMappingInfo info, + // 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) + { + if (elementMapping is not null and not RelationalTypeMapping) + { + return null; + } + + Type concreteCollectionType; + Type? elementType = null; + + if (modelType is not null) + { + // We do GetElementType for multidimensional arrays - these don't implement generic IEnumerable<> + elementType = modelType.TryGetElementType(typeof(IEnumerable<>)) ?? modelType.GetElementType(); + + // E.g. Newtonsoft.Json's JToken is enumerable over itself, exclude that scenario to avoid stack overflow. + if (elementType is null || elementType == modelType || modelType.GetGenericTypeImplementations(typeof(IDictionary<,>)).Any()) + { + return null; + } + } + + var storeType = info.StoreTypeName; + if (storeType is null) + { + if (modelType is null) + { + return null; + } + + // If no mapping was found for the element CLR type, there's no mapping for the array. + // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL + Check.DebugAssert(elementType is not null, "elementClrType is null"); + + var relationalElementMapping = elementMapping as RelationalTypeMapping ?? FindMapping(elementType); + if (relationalElementMapping is not { ElementTypeMapping: null }) + { + return null; + } + + // If the element type mapping is a range, default to return a multirange type mapping (if the PG version supports it). + // Otherwise an array over the range will be returned. + if (_supportsMultiranges) + { + if (relationalElementMapping is GaussDBRangeTypeMapping rangeMapping) + { + var multirangeStoreType = rangeMapping.StoreType switch + { + "int4range" => "int4multirange", + "int8range" => "int8multirange", + "numrange" => "nummultirange", + "tsrange" => "tsmultirange", + "tstzrange" => "tstzmultirange", + "daterange" => "datemultirange", + + _ => throw new InvalidOperationException( + $"Cannot create multirange type mapping for range type '{rangeMapping.StoreType}'") + }; + + return new GaussDBMultirangeTypeMapping(multirangeStoreType, modelType, rangeMapping); + } + + // TODO: This needs to move to the NodaTime plugin, but there's no FindCollectionMapping extension yet for plugins + if (relationalElementMapping.GetType() is + { Name: "IntervalRangeMapping", Namespace: "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type1) + { + return (RelationalTypeMapping)Activator.CreateInstance( + type1.Assembly.GetType( + "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal.IntervalMultirangeMapping")!, + modelType, + relationalElementMapping)!; + } + + if (relationalElementMapping.GetType() is + { Name: "DateIntervalRangeMapping", Namespace: "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type2) + { + return (RelationalTypeMapping)Activator.CreateInstance( + type2.Assembly.GetType( + "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal.DateIntervalMultirangeMapping")!, + modelType, + relationalElementMapping)!; + } + } + + // Not a multirange - map as a PG array type + concreteCollectionType = FindTypeToInstantiate(modelType, elementType); + + return (GaussDBArrayTypeMapping)Activator.CreateInstance( + typeof(GaussDBArrayTypeMapping<,,>).MakeGenericType(modelType, concreteCollectionType, elementType), + relationalElementMapping)!; + } + + if (storeType.EndsWith("[]", StringComparison.Ordinal)) + { + // We have an array store type (either because we're reverse engineering or the user explicitly specified it) + var elementStoreType = storeType.Substring(0, storeType.Length - 2); + + // Note that we ignore the elementMapping argument here (but not in the CLR type-only path above). + // This is because the user-provided storeType for the array should take precedence over the element type mapping that gets + // calculated purely based on the element's CLR type in base.FindMappingWithConversion. + var relationalElementMapping = elementMapping as RelationalTypeMapping + ?? (elementType is null + ? FindMapping(elementStoreType) + : FindMapping(elementType, elementStoreType)); + if (relationalElementMapping is not { ElementTypeMapping: null }) + { + return null; + } + + // If no mapping was found for the element, there's no mapping for the array. + // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL + if (relationalElementMapping is not null and not GaussDBArrayTypeMapping) + { + if (modelType is null) + { + // There's no model type - we're scaffolding purely from the store type. + elementType = relationalElementMapping.ClrType; + modelType = concreteCollectionType = typeof(List<>).MakeGenericType(elementType); + } + else + { + concreteCollectionType = FindTypeToInstantiate(modelType, elementType!); + Check.DebugAssert(elementType is not null, "elementType is null"); + } + + return (GaussDBArrayTypeMapping)Activator.CreateInstance( + typeof(GaussDBArrayTypeMapping<,,>).MakeGenericType(modelType, concreteCollectionType, elementType), + storeType, relationalElementMapping)!; + } + } + else if (IsMultirange(storeType, out var rangeStoreType) && _supportsMultiranges) + { + // Note that we ignore the elementMapping argument here (but not in the CLR type-only path above). + // This is because the user-provided storeType for the array should take precedence over the element type mapping that gets + // calculated purely based on the element's CLR type in base.FindMappingWithConversion. + var relationalElementMapping = elementType is null + ? FindMapping(rangeStoreType) + : FindMapping(elementType, rangeStoreType); + + // If no mapping was found for the element, there's no mapping for the array. + // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL + if (relationalElementMapping is GaussDBRangeTypeMapping rangeMapping + // TODO: Why exclude if there's an element converter?? + && (relationalElementMapping.Converter is null || modelType is null || modelType.IsArrayOrGenericList())) + { + return new GaussDBMultirangeTypeMapping( + storeType, modelType ?? typeof(List<>).MakeGenericType(relationalElementMapping.ClrType), rangeMapping); + } + + // TODO: This needs to move to the NodaTime plugin, but there's no FindCollectionMapping extension yet for plugins + if (relationalElementMapping?.GetType() is + { Name: "IntervalRangeMapping", Namespace: "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type1) + { + return (RelationalTypeMapping)Activator.CreateInstance( + type1.Assembly.GetType( + "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal.IntervalMultirangeMapping")!, + modelType ?? relationalElementMapping.ClrType.MakeArrayType(), + relationalElementMapping)!; + } + + if (relationalElementMapping?.GetType() is + { Name: "DateIntervalRangeMapping", Namespace: "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type2) + { + return (RelationalTypeMapping)Activator.CreateInstance( + type2.Assembly.GetType( + "GaussDB.EntityFrameworkCore.PostgreSQL.Storage.Internal.DateIntervalMultirangeMapping")!, + modelType ?? relationalElementMapping.ClrType.MakeArrayType(), + relationalElementMapping)!; + } + } + + return null; + + static bool IsMultirange(string multiRangeStoreType, [NotNullWhen(true)] out string? rangeStoreType) + { + rangeStoreType = multiRangeStoreType switch + { + "int4multirange" => "int4range", + "int8multirange" => "int8range", + "nummultirange" => "numrange", + "tsmultirange" => "tsrange", + "tstzmultirange" => "tstzrange", + "datemultirange" => "daterange", + _ => null + }; + + return rangeStoreType is not null; + } + + static Type FindTypeToInstantiate(Type collectionType, Type elementType) + { + if (collectionType.IsArray) + { + return collectionType; + } + + var listOfT = typeof(List<>).MakeGenericType(elementType); + + if (collectionType.IsAssignableFrom(listOfT)) + { + if (!collectionType.IsAbstract) + { + var constructor = collectionType.GetDeclaredConstructor(null); + if (constructor?.IsPublic == true) + { + return collectionType; + } + } + + return listOfT; + } + + return collectionType; + } + } + + /// + /// 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? FindRowValueMapping(in RelationalTypeMappingInfo mappingInfo) + => mappingInfo.ClrType is { } clrType + && clrType.IsAssignableTo(typeof(ITuple)) + ? new GaussDBRowValueTypeMapping(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 GaussDBParameter.DataTypeName + // (though see https://github.com/npgsql/npgsql/issues/5710). + return new GaussDBEnumTypeMapping( + _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 + /// 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? FindUserRangeMapping(in RelationalTypeMappingInfo mappingInfo) + { + UserRangeDefinition? rangeDefinition = null; + var rangeStoreType = mappingInfo.StoreTypeName; + var rangeClrType = mappingInfo.ClrType; + + // If the incoming MappingInfo contains a ClrType, make sure it's an GaussDBRange, otherwise bail + if (rangeClrType is not null && (!rangeClrType.IsGenericType || rangeClrType.GetGenericTypeDefinition() != typeof(GaussDBRange<>))) + { + return null; + } + + // Try to find a user range definition (defined by the user on their context options), based on the + // incoming MappingInfo's StoreType or ClrType + if (rangeStoreType is not null) + { + rangeDefinition = _userRangeDefinitions.SingleOrDefault(m => m.StoreTypeName == rangeStoreType); + + if (rangeDefinition is null) + { + return null; + } + + if (rangeClrType is null) + { + // The incoming MappingInfo does not contain a ClrType, only a StoreType (i.e. scaffolding). + // Construct the range ClrType from the range definition's subtype ClrType + rangeClrType = typeof(GaussDBRange<>).MakeGenericType(rangeDefinition.SubtypeClrType); + } + else if (rangeClrType != typeof(GaussDBRange<>).MakeGenericType(rangeDefinition.SubtypeClrType)) + { + // If the incoming MappingInfo also contains a ClrType (in addition to the StoreType), make sure it + // corresponds to the subtype ClrType on the range definition + return null; + } + } + else if (rangeClrType is not null) + { + rangeDefinition = _userRangeDefinitions.SingleOrDefault(m => m.SubtypeClrType == rangeClrType.GetGenericArguments()[0]); + } + + if (rangeClrType is null || rangeDefinition is null) + { + return null; + } + + // We now have a user-defined range definition from the context options. Use it to get the subtype's mapping + var subtypeMapping = rangeDefinition.SubtypeName is null + ? FindMapping(rangeDefinition.SubtypeClrType) + : FindMapping(rangeDefinition.SubtypeName); + + if (subtypeMapping is null) + { + 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 GaussDBParameter.DataTypeName + var quotedRangeStoreType = _sqlGenerationHelper.DelimitIdentifier(rangeDefinition.StoreTypeName, rangeDefinition.StoreTypeSchema); + var unquotedRangeStoreType = rangeDefinition.StoreTypeSchema is null + ? rangeDefinition.StoreTypeName + : rangeDefinition.StoreTypeSchema + '.' + rangeDefinition.StoreTypeName; + + return GaussDBRangeTypeMapping.CreatUserDefinedRangeMapping( + quotedRangeStoreType, unquotedRangeStoreType, rangeClrType, subtypeMapping); + } + + /// + /// Finds the mapping for a container given its CLR type and its containee's type mapping; this is used when inferring type mappings + /// for arrays and ranges/multiranges. + /// + public virtual RelationalTypeMapping? FindContainerMapping( + Type containerClrType, + RelationalTypeMapping containeeTypeMapping, + IModel model) + { + // Ranges aren't handled by the general FindMapping logic below, as we don't represent range type mappings as having an element + // (they're not queryable). + if (containerClrType.IsRange()) + { + var rangeStoreType = containeeTypeMapping.StoreType switch + { + "int" or "integer" => "int4range", + "bigint" => "int8range", + "decimal" or "numeric" => "numrange", + "date" => "daterange", + "timestamp" or "timestamp without time zone" => "tsrange", + "timestamptz" or "timestamp with time zone" => "tstzrange", + _ => null + }; + + 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. + if (FindMapping(containerClrType, model, containeeTypeMapping) is RelationalTypeMapping containerMapping) + { + return containerMapping; + } + + return null; + } + + private static bool NameBasesUsesPrecision(ReadOnlySpan span) + => span.ToString() switch + { + "decimal" => true, + "dec" => true, + "numeric" => true, + "timestamp" => true, + "timestamptz" => true, + "time" => true, + "interval" => true, + _ => false + }; + + /// + /// 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. + /// + // We override to support parsing array store names (e.g. varchar(32)[]), timestamp(5) with time zone, etc. + protected override string? ParseStoreTypeName( + string? storeTypeName, + ref bool? unicode, + ref int? size, + ref int? precision, + ref int? scale) + { + // TODO: Reimplement over ParseStoreTypeName below + + if (storeTypeName is null) + { + return null; + } + + var s = storeTypeName.AsSpan().Trim(); + + // 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). + if (s.EndsWith("[]", StringComparison.Ordinal)) + { + return storeTypeName; + } + + var openParen = s.IndexOf("(", StringComparison.Ordinal); + if (openParen == -1) + { + return storeTypeName; + } + + var preParens = s[..openParen].Trim(); + s = s.Slice(openParen + 1); + var closeParen = s.IndexOf(")", StringComparison.Ordinal); + if (closeParen == -1) + { + return storeTypeName; + } + + 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 storeTypeName; + } + + if (NameBasesUsesPrecision(preParens)) + { + precision = p; + scale = 0; + } + else + { + size = p; + } + + break; + + case var comma: + if (int.TryParse(s[..comma].Trim(), out var parsedPrecision)) + { + precision = parsedPrecision; + } + else + { + return storeTypeName; + } + + if (int.TryParse(s[(comma + 1)..closeParen].Trim(), out var parsedScale)) + { + scale = parsedScale; + } + else + { + return storeTypeName; + } + + break; + } + + if (postParens.Length == 0) + { + return preParens.Length == storeTypeName.Length + ? storeTypeName + : preParens.ToString(); + } + + 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.GaussDB/Storage/Internal/IGaussDBRelationalConnection.cs b/src/EFCore.GaussDB/Storage/Internal/IGaussDBRelationalConnection.cs new file mode 100644 index 0000000000..581f7242b8 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/IGaussDBRelationalConnection.cs @@ -0,0 +1,36 @@ +using System.Data.Common; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.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 interface IGaussDBRelationalConnection : 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 + /// 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. + /// + IGaussDBRelationalConnection CreateAdminConnection(); + + /// + /// 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. + /// + ValueTask CloneWith(string connectionString, bool async, CancellationToken cancellationToken = default); +} diff --git a/src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs b/src/EFCore.GaussDB/Storage/Internal/Json/JsonBitArrayReaderWriter.cs similarity index 97% rename from src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs rename to src/EFCore.GaussDB/Storage/Internal/Json/JsonBitArrayReaderWriter.cs index eeb293210a..f481caa06b 100644 --- a/src/EFCore.PG/Storage/Internal/Json/JsonBitArrayReaderWriter.cs +++ b/src/EFCore.GaussDB/Storage/Internal/Json/JsonBitArrayReaderWriter.cs @@ -2,7 +2,7 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Json; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs b/src/EFCore.GaussDB/Storage/Internal/Json/JsonMacaddrReaderWriter.cs similarity index 96% rename from src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs rename to src/EFCore.GaussDB/Storage/Internal/Json/JsonMacaddrReaderWriter.cs index ca1aa2bc63..f113650492 100644 --- a/src/EFCore.PG/Storage/Internal/Json/JsonMacaddrReaderWriter.cs +++ b/src/EFCore.GaussDB/Storage/Internal/Json/JsonMacaddrReaderWriter.cs @@ -2,7 +2,7 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Json; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBArrayTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBArrayTypeMapping.cs new file mode 100644 index 0000000000..3fdb990466 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBArrayTypeMapping.cs @@ -0,0 +1,345 @@ +using System.Collections; +using System.Data; +using System.Data.Common; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.ValueConversion; +using HuaweiCloud.GaussDBTypes; +using HuaweiCloud.GaussDB; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// Type mapping for GaussDB arrays. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/arrays.html +/// +public abstract class GaussDBArrayTypeMapping : RelationalTypeMapping +{ + /// + /// 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 GaussDBArrayTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping ElementTypeMapping + { + get + { + var elementTypeMapping = base.ElementTypeMapping; + Check.DebugAssert( + elementTypeMapping is not null, + "GaussDBArrayTypeMapping without an element type mapping"); + Check.DebugAssert( + elementTypeMapping is RelationalTypeMapping, + "GaussDBArrayTypeMapping with a non-relational element type mapping"); + return (RelationalTypeMapping)elementTypeMapping; + } + } +} + +/// +/// Type mapping for GaussDB arrays. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/arrays.html +/// +public class GaussDBArrayTypeMapping : GaussDBArrayTypeMapping +{ + /// + /// 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 GaussDBArrayTypeMapping Default { get; } + = new(); + + /// + /// The database type used by GaussDB. + /// + public virtual GaussDBDbType? GaussDBDbType { 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. + /// + [UsedImplicitly] + public GaussDBArrayTypeMapping(RelationalTypeMapping elementTypeMapping) + : this(elementTypeMapping.StoreType + "[]", elementTypeMapping) + { + } + + /// + /// 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. + /// + [UsedImplicitly] + public GaussDBArrayTypeMapping(string storeType, RelationalTypeMapping elementTypeMapping) + : this(CreateParameters(storeType, elementTypeMapping)) + { + Check.DebugAssert(storeType.EndsWith("[]", StringComparison.Ordinal), "GaussDBArrayTypeMapping created for a non-array store type"); + } + + private static RelationalTypeMappingParameters CreateParameters(string storeType, RelationalTypeMapping elementMapping) + { + ValueConverter? converter = null; + + // We do GetElementType for multidimensional arrays - these don't implement generic IEnumerable<> + var elementType = typeof(TCollection).TryGetElementType(typeof(IEnumerable<>)) ?? typeof(TCollection).GetElementType(); + + Check.DebugAssert(elementType is not null, "modelElementType cannot be null"); + + if (elementMapping.Converter is { } elementConverter) + { + var providerElementType = elementConverter.ProviderClrType; + + // Nullability has been unwrapped on the element converter's provider CLR type, so add it back here if needed + if (elementType.IsNullableValueType()) + { + providerElementType = providerElementType.MakeNullable(); + } + + converter = (ValueConverter)Activator.CreateInstance( + typeof(GaussDBArrayConverter<,,>).MakeGenericType( + typeof(TCollection), typeof(TConcreteCollection), providerElementType.MakeArrayType()), + elementConverter)!; + } + else if (typeof(TCollection) != typeof(TConcreteCollection)) + { + converter = (ValueConverter)Activator.CreateInstance( + typeof(GaussDBArrayConverter<,,>).MakeGenericType( + typeof(TCollection), typeof(TConcreteCollection), elementType.MakeArrayType()))!; + } + +#pragma warning disable EF1001 + var comparer = typeof(TCollection).IsArray && typeof(TCollection).GetArrayRank() > 1 + ? null // TODO: Value comparer for multidimensional arrays + : (ValueComparer?)Activator.CreateInstance( + elementType.IsNullableValueType() || elementMapping.Comparer.Type.IsNullableValueType() + ? 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 && !typeof(TElement).UnwrapNullableType().IsAssignableTo(elementJsonReaderWriter.ValueType)) + { + throw new InvalidOperationException( + $"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 + // GaussDBRowValueTypeMapping). + // TODO: Also, we don't (yet) support JSON serialization of multidimensional arrays. + var collectionJsonReaderWriter = + elementJsonReaderWriter is null || typeof(TCollection).IsArray && typeof(TCollection).GetArrayRank() > 1 + ? null + : (JsonValueReaderWriter?)Activator.CreateInstance( + (elementType.IsNullableValueType() + ? typeof(JsonCollectionOfNullableStructsReaderWriter<,>) + : elementType.IsValueType + ? typeof(JsonCollectionOfStructsReaderWriter<,>) + : typeof(JsonCollectionOfReferencesReaderWriter<,>)) + .MakeGenericType(typeof(TConcreteCollection), elementType.UnwrapNullableType()), + elementJsonReaderWriter); + + return new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(TCollection), converter, comparer, elementMapping: elementMapping, + jsonValueReaderWriter: collectionJsonReaderWriter), + storeType); + } + + /// + /// 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 GaussDBArrayTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + var clrType = parameters.CoreParameters.ClrType; + + if (clrType.TryGetElementType(typeof(IEnumerable<>)) == null && clrType.GetElementType() == null) + { + throw new ArgumentException($"CLR type '{parameters.CoreParameters.ClrType}' isn't an IEnumerable"); + } + + // If the element mapping has an GaussDBDbType or DbType, set our own GaussDBDbType as an array of that. + // Otherwise let the ADO.NET layer infer the GaussDB type. We can't always let it infer, otherwise + // when given a byte[] it will infer byte (but we want smallint[]) + GaussDBDbType = GaussDBTypes.GaussDBDbType.Array + | (ElementTypeMapping is IGaussDBTypeMapping { GaussDBDbType: not GaussDBTypes.GaussDBDbType.Unknown } elementGaussDBTypeMapping + ? elementGaussDBTypeMapping.GaussDBDbType + : ElementTypeMapping.DbType.HasValue + ? new GaussDBParameter { DbType = ElementTypeMapping.DbType.Value }.GaussDBDbType + : default(GaussDBDbType?)); + } + + // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled + // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. + private GaussDBArrayTypeMapping() + : base(new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(TCollection), elementMapping: NullMapping), + "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 override DbParameter CreateParameter( + DbCommand command, + string name, + object? value, + bool? nullable = null, + ParameterDirection direction = ParameterDirection.Input) + { + // 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 GaussDB 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 GaussDB support IList<> instead of only arrays and List<> + if (value is not null && Converter is null && !value.GetType().IsArrayOrGenericList()) + { + switch (value) + { + case IEnumerable elements: + value = elements.ToList(); + break; + + case IEnumerable elements: + value = elements.Cast().ToList(); + break; + } + } + + var param = base.CreateParameter(command, name, value, nullable, direction); + if (param is not GaussDBParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {GetType().Name} being used with non-GaussDB parameter type {param.GetType().Name}"); + } + + // Enums and user-defined ranges require setting GaussDBParameter.DataTypeName to specify the GaussDB type name. + // Make this work for arrays over these types as well. + switch (ElementTypeMapping) + { + case GaussDBEnumTypeMapping enumTypeMapping: + npgsqlParameter.DataTypeName = enumTypeMapping.UnquotedStoreType + "[]"; + break; + case GaussDBRangeTypeMapping { UnquotedStoreType: string unquotedStoreType }: + npgsqlParameter.DataTypeName = unquotedStoreType + "[]"; + break; + } + return param; + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + { + Check.DebugAssert( + parameters.CoreParameters.ClrType == typeof(TCollection), "GaussDBArrayTypeMapping.Clone attempting to change ClrType"); + Check.DebugAssert( + parameters.CoreParameters.ElementTypeMapping is not null, "GaussDBArrayTypeMapping.Clone without an element type mapping"); + Check.DebugAssert( + parameters.CoreParameters.ElementTypeMapping.ClrType == typeof(TElement).UnwrapNullableType(), + "GaussDBArrayTypeMapping.Clone attempting to change element ClrType"); + + return new GaussDBArrayTypeMapping(parameters); + } + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + if (value is not IEnumerable enumerable) + { + throw new ArgumentException($"'{value.GetType().Name}' must be an IEnumerable", nameof(value)); + } + + if (value is Array array && array.Rank != 1) + { + throw new NotSupportedException("Multidimensional array literals aren't supported"); + } + + var sb = new StringBuilder(); + sb.Append("ARRAY["); + + var isFirst = true; + foreach (var element in enumerable) + { + if (isFirst) + { + isFirst = false; + } + else + { + sb.Append(","); + } + + sb.Append(ElementTypeMapping.GenerateProviderValueSqlLiteral(element)); + } + + sb.Append("]::"); + sb.Append(ElementTypeMapping.StoreType); + sb.Append("[]"); + return sb.ToString(); + } + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter npgsqlParameter) + { + throw new ArgumentException( + $"GaussDB-specific type mapping {GetType()} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + if (GaussDBDbType.HasValue) + { + npgsqlParameter.GaussDBDbType = GaussDBDbType.Value; + } + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBigIntegerTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBigIntegerTypeMapping.cs new file mode 100644 index 0000000000..f6b7fdf899 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBigIntegerTypeMapping.cs @@ -0,0 +1,107 @@ +using System.Numerics; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBBigIntegerTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBBigIntegerTypeMapping Default { 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 GaussDBBigIntegerTypeMapping() + : base("numeric", typeof(BigInteger), GaussDBDbType.Numeric, jsonValueReaderWriter: JsonBigIntegerReaderWriter.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. + /// + protected GaussDBBigIntegerTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Numeric) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBBigIntegerTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null + ? storeType + : parameters.Scale is null + ? $"numeric({parameters.Precision})" + : $"numeric({parameters.Precision},{parameters.Scale})"; + + /// + /// 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.GaussDB/Storage/Internal/Mapping/GaussDBBitTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBitTypeMapping.cs new file mode 100644 index 0000000000..7e57f17e97 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBitTypeMapping.cs @@ -0,0 +1,91 @@ +using System.Collections; +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the GaussDB bit string type. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-bit.html +/// +public class GaussDBBitTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBBitTypeMapping Default { get; } = new(); + + /// + /// Constructs an instance of the class. + /// + public GaussDBBitTypeMapping() + : base("bit", typeof(BitArray), GaussDBDbType.Bit, jsonValueReaderWriter: JsonBitArrayReaderWriter.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. + /// + protected GaussDBBitTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Bit) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBBitTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var bits = (BitArray)value; + var sb = new StringBuilder(); + sb.Append("B'"); + for (var i = 0; i < bits.Count; i++) + { + sb.Append(bits[i] ? '1' : '0'); + } + + sb.Append('\''); + return sb.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var bits = (BitArray)value; + var exprs = new Expression[bits.Count]; + for (var i = 0; i < bits.Count; i++) + { + exprs[i] = Expression.Constant(bits[i]); + } + + return Expression.New(Constructor, Expression.NewArrayInit(typeof(bool), exprs)); + } + + private static readonly ConstructorInfo Constructor = + typeof(BitArray).GetConstructor([typeof(bool[])])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBoolTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBoolTypeMapping.cs new file mode 100644 index 0000000000..5669ce9c1e --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBBoolTypeMapping.cs @@ -0,0 +1,58 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBBoolTypeMapping : BoolTypeMapping +{ + /// + /// 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 new GaussDBBoolTypeMapping Default { 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 GaussDBBoolTypeMapping() + : base("boolean") + { + } + + /// + /// 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 GaussDBBoolTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBBoolTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => (bool)value ? "TRUE" : "FALSE"; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBByteArrayTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBByteArrayTypeMapping.cs new file mode 100644 index 0000000000..ce0b79cfc4 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBByteArrayTypeMapping.cs @@ -0,0 +1,77 @@ +using System.Globalization; +using System.Text; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBByteArrayTypeMapping : RelationalTypeMapping +{ + /// + /// 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 GaussDBByteArrayTypeMapping Default { 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 GaussDBByteArrayTypeMapping() + : base("bytea", typeof(byte[]), System.Data.DbType.Binary, jsonValueReaderWriter: JsonByteArrayReaderWriter.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. + /// + protected GaussDBByteArrayTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBByteArrayTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + Check.NotNull(value, nameof(value)); + var bytea = (byte[])value; + + var builder = new StringBuilder(bytea.Length * 2 + 6); + + builder.Append("BYTEA E'\\\\x"); + foreach (var b in bytea) + { + builder.Append(b.ToString("X2", CultureInfo.InvariantCulture)); + } + + builder.Append('\''); + + return builder.ToString(); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCharacterCharMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCharacterCharMapping.cs new file mode 100644 index 0000000000..fdf14dbd0c --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCharacterCharMapping.cs @@ -0,0 +1,83 @@ +using System.Data.Common; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDBTypes; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// Type mapping for the GaussDB 'character' data type. Handles both CLR strings and chars. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-character.html +/// +public class GaussDBCharacterCharTypeMapping : CharTypeMapping, IGaussDBTypeMapping +{ + /// + /// 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 new GaussDBCharacterCharTypeMapping Default { get; } = new("text"); + + /// + public virtual GaussDBDbType GaussDBDbType + => GaussDBDbType.Char; + + /// + /// 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 GaussDBCharacterCharTypeMapping(string storeType) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(char), jsonValueReaderWriter: JsonCharReaderWriter.Instance), + storeType, + StoreTypePostfix.Size, + System.Data.DbType.StringFixedLength, + unicode: false, + fixedLength: 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 GaussDBCharacterCharTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBCharacterCharTypeMapping(parameters); + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {GetType().Name} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + base.ConfigureParameter(parameter); + npgsqlParameter.GaussDBDbType = GaussDBDbType; + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCharacterStringTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCharacterStringTypeMapping.cs new file mode 100644 index 0000000000..53ec318e4a --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCharacterStringTypeMapping.cs @@ -0,0 +1,148 @@ +using System.Data.Common; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// Type mapping for the GaussDB 'character' data type. Handles both CLR strings and chars. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-character.html +/// +/// +public class GaussDBCharacterStringTypeMapping : GaussDBStringTypeMapping +{ + /// + /// 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 new GaussDBCharacterStringTypeMapping Default { get; } = new("text"); + + /// + /// Static for fixed-width character types. + /// + /// + ///

+ /// Comparisons of 'character' data as defined in the SQL standard differ dramatically from CLR string + /// comparisons. This value comparer adjusts for this by only comparing strings after truncating trailing + /// whitespace. + ///

+ ///

+ /// Note that if a value converter is used and the CLR type isn't a string at all, we just use the default + /// value converter instead. + ///

+ ///
+ private static readonly ValueComparer CharacterValueComparer = + new( + (x, y) => EqualsWithoutTrailingWhitespace(x, y), + x => GetHashCodeWithoutTrailingWhitespace(x)); + + /// + /// 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 ValueComparer Comparer + => ClrType == typeof(string) ? CharacterValueComparer : base.Comparer; + + /// + /// 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 ValueComparer KeyComparer + => Comparer; + + /// + /// 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 GaussDBCharacterStringTypeMapping(string storeType, int size = 1) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(string), jsonValueReaderWriter: JsonStringReaderWriter.Instance), + storeType, + StoreTypePostfix.Size, + System.Data.DbType.StringFixedLength, + unicode: false, + size, + fixedLength: 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 GaussDBCharacterStringTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Char) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBCharacterStringTypeMapping( + new RelationalTypeMappingParameters( + parameters.CoreParameters, + parameters.StoreType, + StoreTypePostfix.Size, + parameters.DbType, + parameters.Unicode, + parameters.Size, + parameters.FixedLength, + parameters.Precision, + parameters.Scale)); + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter.Value is string value) + { + parameter.Value = value.TrimEnd(); + } + + base.ConfigureParameter(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. + /// + public static bool EqualsWithoutTrailingWhitespace(string? a, string? b) + => (a, b) switch + { + (null, null) => true, + (_, null) => false, + (null, _) => false, + _ => a.AsSpan().TrimEnd().SequenceEqual(b.AsSpan().TrimEnd()) + }; + + /// + /// 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.GaussDB/Storage/Internal/Mapping/GaussDBCidrTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCidrTypeMapping.cs new file mode 100644 index 0000000000..8eb8f91777 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBCidrTypeMapping.cs @@ -0,0 +1,130 @@ +using System.Net; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the PostgreSQL cidr type. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-CIDR +/// +public class GaussDBCidrTypeMapping : GaussDBTypeMapping +{ + /// + /// 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] + public static GaussDBCidrTypeMapping Default { 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. + /// + [Obsolete] + public GaussDBCidrTypeMapping() + : base("cidr", typeof(GaussDBCidr), GaussDBDbType.Cidr, JsonCidrReaderWriter.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. + /// + protected GaussDBCidrTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Cidr) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBCidrTypeMapping(parameters); + + // + // 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] + //protected override string GenerateNonNullSqlLiteral(object value) + //{ + // var cidr = (GaussDBCidr)value; + // return $"CIDR '{cidr.Address}/{cidr.Netmask}'"; + //} + + // + // 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] + //public override Expression GenerateCodeLiteral(object value) + //{ + // var cidr = (GaussDBCidr)value; + // return Expression.New( + // GaussDBCidrConstructor, + // Expression.Call(ParseMethod, Expression.Constant(cidr.Address.ToString())), + // Expression.Constant(cidr.Netmask)); + //} + + private static readonly MethodInfo ParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; + [Obsolete] + private static readonly ConstructorInfo GaussDBCidrConstructor = + typeof(GaussDBCidr).GetConstructor([typeof(IPAddress), typeof(byte)])!; + + /// + /// 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] + 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 GaussDBCidr 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, GaussDBCidr value) + => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDateOnlyTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDateOnlyTypeMapping.cs new file mode 100644 index 0000000000..a84f7efcbb --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDateOnlyTypeMapping.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBDateOnlyTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBDateOnlyTypeMapping Default { 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 GaussDBDateOnlyTypeMapping() + : base("date", typeof(DateOnly), GaussDBDbType.Date, GaussDBJsonDateOnlyReaderWriter.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. + /// + protected GaussDBDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Date) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBDateOnlyTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"DATE '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => Format((DateOnly)value); + + private static string Format(DateOnly date) + { + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (date == DateOnly.MinValue) + { + return "-infinity"; + } + + if (date == DateOnly.MaxValue) + { + return "infinity"; + } + } + + return date.ToString("yyyy-MM-dd", 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 sealed class GaussDBJsonDateOnlyReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty = typeof(GaussDBJsonDateOnlyReaderWriter).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 GaussDBJsonDateOnlyReaderWriter 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()!; + + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateOnly.MinValue; + case "infinity": + return DateOnly.MaxValue; + } + } + + 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.GaussDB/Storage/Internal/Mapping/GaussDBDateTimeDateTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDateTimeDateTypeMapping.cs new file mode 100644 index 0000000000..eb5834347c --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDateTimeDateTypeMapping.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBDateTimeDateTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBDateTimeDateTypeMapping Default { 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 GaussDBDateTimeDateTypeMapping() + : base("date", typeof(DateTime), GaussDBDbType.Date, GaussDBJsonDateTimeReaderWriter.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. + /// + protected GaussDBDateTimeDateTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Date) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBDateTimeDateTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"DATE '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => Format((DateTime)value); + + private static string Format(DateTime date) + { + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (date == DateTime.MinValue) + { + return "-infinity"; + } + + if (date == DateTime.MaxValue) + { + return "infinity"; + } + } + + return date.ToString("yyyy-MM-dd", 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 sealed class GaussDBJsonDateTimeReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty = typeof(GaussDBJsonDateTimeReaderWriter).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 GaussDBJsonDateTimeReaderWriter 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()!; + + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateTime.MinValue; + case "infinity": + return DateTime.MaxValue; + } + } + + 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.GaussDB/Storage/Internal/Mapping/GaussDBDecimalTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDecimalTypeMapping.cs new file mode 100644 index 0000000000..cdbcaaa8d2 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDecimalTypeMapping.cs @@ -0,0 +1,87 @@ +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBDecimalTypeMapping : GaussDBTypeMapping +{ + private const string DecimalFormatConst = "{0:0.0###########################}"; + + /// + /// 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 GaussDBDecimalTypeMapping Default { 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 GaussDBDecimalTypeMapping(Type? clrType = null) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + clrType ?? typeof(decimal), + jsonValueReaderWriter: clrType == typeof(decimal) || clrType is null + ? JsonDecimalReaderWriter.Instance + : clrType == typeof(double) + ? JsonDoubleReaderWriter.Instance + : clrType == typeof(float) + ? JsonFloatReaderWriter.Instance + : throw new ArgumentException("clrType must be decimal, double or float", nameof(clrType)) + ), + "numeric")) + { + } + + /// + /// 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 GaussDBDecimalTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Numeric) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBDecimalTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null + ? storeType + : parameters.Scale is null + ? $"numeric({parameters.Precision})" + : $"numeric({parameters.Precision},{parameters.Scale})"; + + /// + /// 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 SqlLiteralFormatString + => DecimalFormatConst; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDoubleTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDoubleTypeMapping.cs new file mode 100644 index 0000000000..7f4cbfdfa4 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBDoubleTypeMapping.cs @@ -0,0 +1,64 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBDoubleTypeMapping : DoubleTypeMapping +{ + /// + /// 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 new GaussDBDoubleTypeMapping Default { 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 GaussDBDoubleTypeMapping() + : base("double precision", System.Data.DbType.Double) + { + } + + /// + /// 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 GaussDBDoubleTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBDoubleTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => Convert.ToDouble(value) switch + { + var d when double.IsNaN(d) => "'NaN'", + var d when double.IsPositiveInfinity(d) => "'Infinity'", + var d when double.IsNegativeInfinity(d) => "'-Infinity'", + var d => base.GenerateNonNullSqlLiteral(d) + }; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBEStringTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBEStringTypeMapping.cs new file mode 100644 index 0000000000..edad360b03 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBEStringTypeMapping.cs @@ -0,0 +1,40 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// Represents a so-called GaussDB E-string literal string, which allows C-style escape sequences. +/// This is a "virtual" type mapping which is never returned by . +/// It is only used internally by some method translators to produce literal strings. +/// +/// +/// See https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS +/// +public class GaussDBEStringTypeMapping : StringTypeMapping +{ + /// + /// 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 new static GaussDBEStringTypeMapping Default { 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 GaussDBEStringTypeMapping() + : base("does_not_exist", System.Data.DbType.String) + { + } + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"E'{EscapeSqlLiteral((string)value)}'"; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBEnumTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBEnumTypeMapping.cs new file mode 100644 index 0000000000..1e13db40b3 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBEnumTypeMapping.cs @@ -0,0 +1,184 @@ +using System.Data.Common; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBEnumTypeMapping : RelationalTypeMapping +{ + /// + /// Maps the CLR member values to the GaussDB 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 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 virtual string UnquotedStoreType { 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 static GaussDBEnumTypeMapping Default { 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 GaussDBEnumTypeMapping( + string quotedStoreType, + string unquotedStoreType, + Type enumType, + IReadOnlyDictionary labels) + : base( + quotedStoreType, + enumType, + jsonValueReaderWriter: (JsonValueReaderWriter?)Activator.CreateInstance( + typeof(JsonPgEnumReaderWriter<>).MakeGenericType(enumType))) + { + if (!enumType.IsEnum || !enumType.IsValueType) + { + throw new ArgumentException($"Enum type mappings require a CLR enum. {enumType.FullName} is not an enum."); + } + + UnquotedStoreType = unquotedStoreType; + Labels = 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. + /// + protected GaussDBEnumTypeMapping( + RelationalTypeMappingParameters parameters, + string unquotedStoreType, + IReadOnlyDictionary labels) + : base(parameters) + { + UnquotedStoreType = unquotedStoreType; + Labels = labels; + } + + // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled + // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. + private GaussDBEnumTypeMapping() + : base("some_enum", typeof(int)) + { + UnquotedStoreType = "some_enum"; + Labels = new Dictionary(); + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBEnumTypeMapping(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 GaussDBEnumTypeMapping 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 + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {GetType().Name} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + npgsqlParameter.DataTypeName = UnquotedStoreType; + } + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"'{Labels[value]}'::{StoreType}"; + + // This is public for 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 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 + /// 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 T FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => Enum.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, T value) + => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBFloatTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBFloatTypeMapping.cs new file mode 100644 index 0000000000..c6d6720c3f --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBFloatTypeMapping.cs @@ -0,0 +1,76 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBFloatTypeMapping : FloatTypeMapping +{ + /// + /// 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 new GaussDBFloatTypeMapping Default { 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 GaussDBFloatTypeMapping() + : base("real", System.Data.DbType.Single) + { + } + + /// + /// 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 GaussDBFloatTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBFloatTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var singleValue = Convert.ToSingle(value); + if (double.IsNaN(singleValue)) + { + return "'NaN'"; + } + + if (double.IsPositiveInfinity(singleValue)) + { + return "'Infinity'"; + } + + if (double.IsNegativeInfinity(singleValue)) + { + return "'-Infinity'"; + } + + return base.GenerateNonNullSqlLiteral(singleValue); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBGeometricTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBGeometricTypeMapping.cs new file mode 100644 index 0000000000..99c0b9e755 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBGeometricTypeMapping.cs @@ -0,0 +1,597 @@ +using System.Globalization; +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBPointTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBPointTypeMapping Default { 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 GaussDBPointTypeMapping() + : base("point", typeof(GaussDBPoint), GaussDBDbType.Point) + { + } + + /// + /// 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 GaussDBPointTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Point) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBPointTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var point = (GaussDBPoint)value; + return FormattableString.Invariant($"POINT '({point.X:G17},{point.Y:G17})'"); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var point = (GaussDBPoint)value; + return Expression.New(Constructor, Expression.Constant(point.X), Expression.Constant(point.Y)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBPoint).GetConstructor([typeof(double), typeof(double)])!; +} + +/// +/// 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 GaussDBLineTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBLineTypeMapping Default { 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 GaussDBLineTypeMapping() + : base("line", typeof(GaussDBLine), GaussDBDbType.Line) + { + } + + /// + /// 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 GaussDBLineTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Line) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBLineTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var line = (GaussDBLine)value; + var a = line.A.ToString("G17", CultureInfo.InvariantCulture); + var b = line.B.ToString("G17", CultureInfo.InvariantCulture); + var c = line.C.ToString("G17", CultureInfo.InvariantCulture); + return $"LINE '{{{a},{b},{c}}}'"; + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var line = (GaussDBLine)value; + return Expression.New( + Constructor, + Expression.Constant(line.A), Expression.Constant(line.B), Expression.Constant(line.C)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBLine).GetConstructor([typeof(double), typeof(double), typeof(double)])!; +} + +/// +/// 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 GaussDBLineSegmentTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBLineSegmentTypeMapping Default { 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 GaussDBLineSegmentTypeMapping() + : base("lseg", typeof(GaussDBLSeg), GaussDBDbType.LSeg) + { + } + + /// + /// 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 GaussDBLineSegmentTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.LSeg) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBLineSegmentTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var lseg = (GaussDBLSeg)value; + return FormattableString.Invariant($"LSEG '[({lseg.Start.X:G17},{lseg.Start.Y:G17}),({lseg.End.X:G17},{lseg.End.Y:G17})]'"); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var lseg = (GaussDBLSeg)value; + return Expression.New( + Constructor, + Expression.Constant(lseg.Start.X), Expression.Constant(lseg.Start.Y), + Expression.Constant(lseg.End.X), Expression.Constant(lseg.End.Y)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBLSeg).GetConstructor([typeof(double), typeof(double), typeof(double), typeof(double)])!; +} + +/// +/// 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 GaussDBBoxTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBBoxTypeMapping Default { 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 GaussDBBoxTypeMapping() + : base("box", typeof(GaussDBBox), GaussDBDbType.Box) + { + } + + /// + /// 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 GaussDBBoxTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Box) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBBoxTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var box = (GaussDBBox)value; + return FormattableString.Invariant($"BOX '(({box.Right:G17},{box.Top:G17}),({box.Left:G17},{box.Bottom:G17}))'"); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var box = (GaussDBBox)value; + return Expression.New( + Constructor, + Expression.Constant(box.Top), Expression.Constant(box.Right), + Expression.Constant(box.Bottom), Expression.Constant(box.Left)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBBox).GetConstructor([typeof(double), typeof(double), typeof(double), typeof(double)])!; +} + +/// +/// 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 GaussDBPathTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBPathTypeMapping Default { 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 GaussDBPathTypeMapping() + : base("path", typeof(GaussDBPath), GaussDBDbType.Path) + { + } + + /// + /// 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 GaussDBPathTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Path) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBPathTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var path = (GaussDBPath)value; + var sb = new StringBuilder(); + sb.Append("PATH '"); + sb.Append(path.Open ? '[' : '('); + for (var i = 0; i < path.Count; i++) + { + sb.Append('('); + sb.Append(path[i].X.ToString("G17", CultureInfo.InvariantCulture)); + sb.Append(','); + sb.Append(path[i].Y.ToString("G17", CultureInfo.InvariantCulture)); + sb.Append(')'); + if (i < path.Count - 1) + { + sb.Append(','); + } + } + + sb.Append(path.Open ? ']' : ')'); + sb.Append('\''); + return sb.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var path = (GaussDBPath)value; + return Expression.New( + Constructor, + Expression.NewArrayInit( + typeof(GaussDBPoint), + path.Select( + p => Expression.New( + PointConstructor, + Expression.Constant(p.X), Expression.Constant(p.Y)))), + Expression.Constant(path.Open)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBPath).GetConstructor([typeof(IEnumerable), typeof(bool)])!; + + private static readonly ConstructorInfo PointConstructor = + typeof(GaussDBPoint).GetConstructor([typeof(double), typeof(double)])!; +} + +/// +/// 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 GaussDBPolygonTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBPolygonTypeMapping Default { 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 GaussDBPolygonTypeMapping() + : base("polygon", typeof(GaussDBPolygon), GaussDBDbType.Polygon) + { + } + + /// + /// 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 GaussDBPolygonTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Polygon) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBPolygonTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var polygon = (GaussDBPolygon)value; + var sb = new StringBuilder(); + sb.Append("POLYGON '("); + for (var i = 0; i < polygon.Count; i++) + { + sb.Append('('); + sb.Append(polygon[i].X.ToString("G17", CultureInfo.InvariantCulture)); + sb.Append(','); + sb.Append(polygon[i].Y.ToString("G17", CultureInfo.InvariantCulture)); + sb.Append(')'); + if (i < polygon.Count - 1) + { + sb.Append(','); + } + } + + sb.Append(")'"); + return sb.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var polygon = (GaussDBPolygon)value; + return Expression.New( + Constructor, + Expression.NewArrayInit( + typeof(GaussDBPoint), + polygon.Select( + p => Expression.New( + PointConstructor, + Expression.Constant(p.X), Expression.Constant(p.Y))))); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBPolygon).GetConstructor([typeof(GaussDBPoint[])])!; + + private static readonly ConstructorInfo PointConstructor = + typeof(GaussDBPoint).GetConstructor([typeof(double), typeof(double)])!; +} + +/// +/// 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 GaussDBCircleTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBCircleTypeMapping Default { 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 GaussDBCircleTypeMapping() + : base("circle", typeof(GaussDBCircle), GaussDBDbType.Circle) + { + } + + /// + /// 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 GaussDBCircleTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Circle) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBCircleTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var circle = (GaussDBCircle)value; + return FormattableString.Invariant($"CIRCLE '<({circle.X:G17},{circle.Y:G17}),{circle.Radius:G17}>'"); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var circle = (GaussDBCircle)value; + return Expression.New( + Constructor, + Expression.Constant(circle.X), Expression.Constant(circle.Y), Expression.Constant(circle.Radius)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBCircle).GetConstructor([typeof(double), typeof(double), typeof(double)])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBHstoreTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBHstoreTypeMapping.cs new file mode 100644 index 0000000000..33c7b2eedd --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBHstoreTypeMapping.cs @@ -0,0 +1,140 @@ +using System.Collections.Immutable; +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the GaussDB hstore type. Supports both +/// and over strings. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/hstore.html +/// +public class GaussDBHstoreTypeMapping : GaussDBTypeMapping +{ + private static readonly HstoreMutableComparer MutableComparerInstance = 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 static GaussDBHstoreTypeMapping Default { get; } = new(typeof(Dictionary)); + + /// + /// 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 GaussDBHstoreTypeMapping(Type clrType) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(clrType, comparer: GetComparer(clrType)), + "hstore"), + GaussDBDbType.Hstore) + { + } + + /// + /// 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 GaussDBHstoreTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Hstore) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBHstoreTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var sb = new StringBuilder("HSTORE '"); + foreach (var kv in (IReadOnlyDictionary)value) + { + sb.Append('"'); + sb.Append(kv.Key); // TODO: Escape + sb.Append("\"=>"); + if (kv.Value is null) + { + sb.Append("NULL"); + } + else + { + sb.Append('"'); + sb.Append(kv.Value); // TODO: Escape + sb.Append("\","); + } + } + + sb.Remove(sb.Length - 1, 1); + + sb.Append('\''); + return sb.ToString(); + } + + private static ValueComparer? GetComparer(Type clrType) + { + if (clrType == typeof(Dictionary)) + { + return MutableComparerInstance; + } + + if (clrType == typeof(ImmutableDictionary)) + { + // Because ImmutableDictionary is immutable, we can use the default value comparer, which doesn't + // clone for snapshot and just does reference comparison. + // We could compare contents here if the references are different, but that would penalize the 99% case + // where a different reference means different contents, which would only save a very rare database update. + return null; + } + + throw new ArgumentException( + $"CLR type must be {nameof(Dictionary)} or {nameof(ImmutableDictionary)}"); + } + + private sealed class HstoreMutableComparer() : ValueComparer>( + (a, b) => Compare(a, b), + o => o.GetHashCode(), + o => new Dictionary(o)) + { + private static bool Compare(Dictionary? a, Dictionary? b) + { + if (a is null) + { + return b is null; + } + + if (b is null || a.Count != b.Count) + { + return false; + } + + foreach (var kv in a) + { + if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBInetTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBInetTypeMapping.cs new file mode 100644 index 0000000000..86f3aef4d3 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBInetTypeMapping.cs @@ -0,0 +1,168 @@ +using System.Net; +using System.Text.Json; +using HuaweiCloud.GaussDBTypes; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the GaussDB inet type. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-INET +/// +public class GaussDBInetTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBInetTypeMapping Default { get; } = new(typeof(IPAddress)); + + /// + /// 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 GaussDBInetTypeMapping(Type clrType) + : base( + "inet", + clrType, + GaussDBDbType.Inet, + jsonValueReaderWriter: clrType == typeof(IPAddress) + ? JsonIPAddressReaderWriter.Instance + : clrType == typeof(GaussDBInet) + ? JsonGaussDBInetReaderWriter.Instance + : throw new ArgumentException($"Only {nameof(IPAddress)} and {nameof(GaussDBInet)} are supported", nameof(clrType))) + { + } + + /// + /// 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 GaussDBInetTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Inet) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBInetTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"INET '{value}'"; + + /// + /// 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 Expression GenerateCodeLiteral(object value) + => value switch + { + IPAddress ip => Expression.Call(IPAddressParseMethod, Expression.Constant(ip.ToString())), + GaussDBInet ip => Expression.New(GaussDBInetConstructor, Expression.Constant(ip.ToString())), + _ => throw new UnreachableException() + }; + + private static readonly MethodInfo IPAddressParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; + private static readonly ConstructorInfo GaussDBInetConstructor = typeof(GaussDBInet).GetConstructor([typeof(string)])!; + + /// + /// 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); + } + + /// + /// 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 JsonGaussDBInetReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty = typeof(JsonGaussDBInetReaderWriter).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 JsonGaussDBInetReaderWriter 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 GaussDBInet 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, GaussDBInet value) + => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBIntervalTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBIntervalTypeMapping.cs new file mode 100644 index 0000000000..940002b2f8 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBIntervalTypeMapping.cs @@ -0,0 +1,199 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBIntervalTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBIntervalTypeMapping Default { 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 GaussDBIntervalTypeMapping() + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(TimeSpan), jsonValueReaderWriter: GaussDBJsonTimeSpanReaderWriter.Instance), + "interval")) + { + } + + /// + /// 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 GaussDBIntervalTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Interval) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBIntervalTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null ? storeType : $"interval({parameters.Precision})"; + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"INTERVAL '{FormatTimeSpanAsInterval((TimeSpan)value)}'"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => $""" + "{FormatTimeSpanAsInterval((TimeSpan)value)}" + """; + + /// + /// 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 FormatTimeSpanAsInterval(TimeSpan ts) + => ts.ToString( + $@"{(ts < TimeSpan.Zero ? "\\-" : "")}{(ts.Days == 0 ? "" : "d\\ ")}hh\:mm\:ss{(ts.Ticks % 10000000 == 0 ? "" : "\\.FFFFFF")}", + 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 static TimeSpan ParseIntervalAsTimeSpan(ReadOnlySpan s) + { + // We store the a PG-compatible interval representation in JSON so it can be queried out, but unfortunately this isn't + // compatible with TimeSpan, so we have to parse manually. + + // Note + var isNegated = false; + if (s[0] == '-') + { + isNegated = true; + s = s[1..]; + } + + int i; + for (i = 0; i < s.Length; i++) + { + if (!char.IsDigit(s[i])) + { + break; + } + } + + if (i == s.Length) + { + throw new FormatException(); + } + + // Space is the separator between the days and the hours:minutes:seconds components + var days = 0; + if (s[i] == ' ') + { + days = int.Parse(s[..i]); + s = s[i..]; + } + + var timeSpan = TimeSpan.Parse(s, CultureInfo.InvariantCulture); + + // We've already extracted the days, so there shouldn't be a days component in the rest (indicates an incompatible/malformed string) + if (timeSpan.Days != 0) + { + throw new FormatException(); + } + + if (days > 0) + { + timeSpan = new TimeSpan( + days, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds, timeSpan.Microseconds); + } + + if (isNegated) + { + timeSpan = -timeSpan; + } + + return timeSpan; + } + + /// + /// 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 GaussDBJsonTimeSpanReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty = typeof(GaussDBJsonTimeSpanReaderWriter).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 GaussDBJsonTimeSpanReaderWriter 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.GaussDB/Storage/Internal/Mapping/GaussDBJsonTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBJsonTypeMapping.cs new file mode 100644 index 0000000000..31c214e336 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBJsonTypeMapping.cs @@ -0,0 +1,131 @@ +using System.Text; +using System.Text.Json; +using HuaweiCloud.GaussDBTypes; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// Supports the older GaussDB-specific JSON mapping, allowing mapping json/jsonb to text, to e.g. +/// (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled). +/// For the standard EF JSON support, which relies on owned entity modeling, see . +/// +public class GaussDBJsonTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBJsonTypeMapping Default { get; } = new("jsonb", typeof(JsonElement)); + + /// + /// 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 GaussDBJsonTypeMapping(string storeType, Type clrType) + : base(storeType, clrType, storeType == "jsonb" ? GaussDBDbType.Jsonb : GaussDBDbType.Json) + { + if (storeType != "json" && storeType != "jsonb") + { + throw new ArgumentException($"{nameof(storeType)} must be 'json' or 'jsonb'", nameof(storeType)); + } + } + + /// + /// 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 GaussDBJsonTypeMapping(RelationalTypeMappingParameters parameters, GaussDBDbType npgsqlDbType) + : base(parameters, npgsqlDbType) + { + } + + /// + /// 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 bool IsJsonb + => StoreType == "jsonb"; + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBJsonTypeMapping(parameters, GaussDBDbType); + + /// + /// 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 string EscapeSqlLiteral(string literal) + => Check.NotNull(literal, nameof(literal)).Replace("'", "''"); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + switch (value) + { + case JsonDocument _: + case JsonElement _: + { + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + if (value is JsonDocument doc) + { + doc.WriteTo(writer); + } + else + { + ((JsonElement)value).WriteTo(writer); + } + + writer.Flush(); + return $"'{EscapeSqlLiteral(Encoding.UTF8.GetString(stream.ToArray()))}'"; + } + case string s: + return $"'{EscapeSqlLiteral(s)}'"; + default: // User POCO + return $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'"; + } + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + => value switch + { + JsonDocument document => Expression.Call( + ParseMethod, Expression.Constant(document.RootElement.ToString()), DefaultJsonDocumentOptions), + JsonElement element => Expression.Property( + Expression.Call(ParseMethod, Expression.Constant(element.ToString()), DefaultJsonDocumentOptions), + nameof(JsonDocument.RootElement)), + string s => Expression.Constant(s), + _ => throw new NotSupportedException("Cannot generate code literals for JSON POCOs") + }; + + private static readonly Expression DefaultJsonDocumentOptions = Expression.New(typeof(JsonDocumentOptions)); + + private static readonly MethodInfo ParseMethod = + typeof(JsonDocument).GetMethod(nameof(JsonDocument.Parse), [typeof(string), typeof(JsonDocumentOptions)])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBLTreeTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBLTreeTypeMapping.cs new file mode 100644 index 0000000000..14ea39d081 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBLTreeTypeMapping.cs @@ -0,0 +1,110 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBLTreeTypeMapping : GaussDBStringTypeMapping +{ + 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 + /// 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 new GaussDBLTreeTypeMapping Default { 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 GaussDBLTreeTypeMapping() + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(LTree), + new ValueConverter(l => l, s => new LTree(s)), + jsonValueReaderWriter: JsonLTreeReaderWriter.Instance), + "ltree"), + GaussDBDbType.LTree) + { + } + + /// + /// 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 GaussDBLTreeTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.LTree) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBLTreeTypeMapping(parameters); + + /// + /// 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 Expression GenerateCodeLiteral(object value) + => Expression.New(Constructor, Expression.Constant((string)(LTree)value, typeof(string))); + + /// + /// 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.GaussDB/Storage/Internal/Mapping/GaussDBMacaddr8TypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMacaddr8TypeMapping.cs new file mode 100644 index 0000000000..2ea603d190 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMacaddr8TypeMapping.cs @@ -0,0 +1,76 @@ +using System.Net.NetworkInformation; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the GaussDB macaddr8 type. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-MACADDR8 +/// +public class GaussDBMacaddr8TypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBMacaddr8TypeMapping Default { 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 GaussDBMacaddr8TypeMapping() + : base( + "macaddr8", + typeof(PhysicalAddress), + GaussDBDbType.MacAddr8, + jsonValueReaderWriter: JsonMacaddrReaderWriter.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. + /// + protected GaussDBMacaddr8TypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.MacAddr8) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBMacaddr8TypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"MACADDR8 '{(PhysicalAddress)value}'"; + + /// + /// 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 Expression GenerateCodeLiteral(object value) + => Expression.Call(ParseMethod, Expression.Constant(((PhysicalAddress)value).ToString())); + + private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", [typeof(string)])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMacaddrTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMacaddrTypeMapping.cs new file mode 100644 index 0000000000..4df8894f66 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMacaddrTypeMapping.cs @@ -0,0 +1,72 @@ +using System.Net.NetworkInformation; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the GaussDB macaddr type. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-MACADDR +/// +public class GaussDBMacaddrTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBMacaddrTypeMapping Default { 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 GaussDBMacaddrTypeMapping() + : base("macaddr", typeof(PhysicalAddress), GaussDBDbType.MacAddr, jsonValueReaderWriter: JsonMacaddrReaderWriter.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. + /// + protected GaussDBMacaddrTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.MacAddr) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBMacaddrTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"MACADDR '{(PhysicalAddress)value}'"; + + /// + /// 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 Expression GenerateCodeLiteral(object value) + => Expression.Call(ParseMethod, Expression.Constant(((PhysicalAddress)value).ToString())); + + private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", [typeof(string)])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMoneyTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMoneyTypeMapping.cs new file mode 100644 index 0000000000..bb7f6b9a86 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMoneyTypeMapping.cs @@ -0,0 +1,58 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBMoneyTypeMapping : DecimalTypeMapping +{ + /// + /// 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 new GaussDBMoneyTypeMapping Default { 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 GaussDBMoneyTypeMapping() + : base("money", System.Data.DbType.Currency) + { + } + + /// + /// 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 GaussDBMoneyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBMoneyTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => base.GenerateNonNullSqlLiteral(value) + "::money"; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMultirangeTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMultirangeTypeMapping.cs new file mode 100644 index 0000000000..cf2e4a1e1e --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBMultirangeTypeMapping.cs @@ -0,0 +1,168 @@ +using System.Collections; +using System.Data.Common; +using System.Text; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDBTypes; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for GaussDB multirange types. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/rangetypes.html +/// +public class GaussDBMultirangeTypeMapping : RelationalTypeMapping +{ + /// + /// The relational type mapping of the ranges contained in this multirange. + /// + public virtual GaussDBRangeTypeMapping RangeMapping + => (GaussDBRangeTypeMapping)ElementTypeMapping!; + + /// + /// The relational type mapping of the values contained in this multirange. + /// + public virtual RelationalTypeMapping SubtypeMapping { get; } + + /// + /// The database type used by GaussDB. + /// + public virtual GaussDBDbType GaussDBDbType { get; } + + /// + /// Constructs an instance of the class. + /// + /// The database type to map + /// The CLR type to map. + /// The type mapping of the ranges contained in this multirange. + public GaussDBMultirangeTypeMapping(string storeType, Type clrType, GaussDBRangeTypeMapping rangeMapping) + // TODO: Need to do comparer, converter + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(clrType, elementMapping: rangeMapping), + storeType)) + { + SubtypeMapping = rangeMapping.SubtypeMapping; + GaussDBDbType = GenerateGaussDBDbType(rangeMapping.SubtypeMapping); + } + + /// + /// 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 GaussDBMultirangeTypeMapping( + RelationalTypeMappingParameters parameters, + GaussDBDbType npgsqlDbType) + : base(parameters) + { + var rangeMapping = (GaussDBRangeTypeMapping)parameters.CoreParameters.ElementTypeMapping!; + + SubtypeMapping = rangeMapping.SubtypeMapping; + GaussDBDbType = npgsqlDbType; + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBMultirangeTypeMapping(parameters, GaussDBDbType); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => GenerateNonNullSqlLiteral(value, RangeMapping, StoreType); + + /// + /// 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 GenerateNonNullSqlLiteral(object value, RelationalTypeMapping rangeMapping, string multirangeStoreType) + { + var multirange = (IList)value; + + var sb = new StringBuilder(); + sb.Append("'{"); + + for (var i = 0; i < multirange.Count; i++) + { + sb.Append(rangeMapping.GenerateEmbeddedSqlLiteral(multirange[i])); + if (i < multirange.Count - 1) + { + sb.Append(", "); + } + } + + sb.Append("}'::"); + sb.Append(multirangeStoreType); + return sb.ToString(); + } + + private static GaussDBDbType GenerateGaussDBDbType(RelationalTypeMapping subtypeMapping) + { + GaussDBDbType subtypeGaussDBDbType; + if (subtypeMapping is IGaussDBTypeMapping npgsqlTypeMapping) + { + subtypeGaussDBDbType = npgsqlTypeMapping.GaussDBDbType; + } + else + { + // We're using a built-in, non-GaussDB mapping such as IntTypeMapping. + // Infer the GaussDBDbType from the DbType (somewhat hacky but why not). + Debug.Assert(subtypeMapping.DbType.HasValue); + var p = new GaussDBParameter { DbType = subtypeMapping.DbType.Value }; + subtypeGaussDBDbType = p.GaussDBDbType; + } + + return GaussDBDbType.Multirange | subtypeGaussDBDbType; + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + // Note that arrays are handled in EF Core's CSharpHelper, so this method doesn't get called for them. + + // Unfortunately, List> requires MemberInit, which CSharpHelper doesn't support + var type = value.GetType(); + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + throw new NotSupportedException("Cannot generate code literals for List, consider using arrays instead"); + } + + throw new InvalidCastException(); + } + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter npgsqlParameter) + { + throw new ArgumentException( + $"GaussDB-specific type mapping {GetType()} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + npgsqlParameter.GaussDBDbType = GaussDBDbType; + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBOwnedJsonTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBOwnedJsonTypeMapping.cs new file mode 100644 index 0000000000..7171ad5b81 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBOwnedJsonTypeMapping.cs @@ -0,0 +1,129 @@ +using System.Data.Common; +using System.Text; +using System.Text.Json; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDBTypes; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// Supports the standard EF JSON support, which relies on owned entity modeling. +/// See for the older GaussDB-specific support, which allows mapping json/jsonb to text, to e.g. +/// (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled). +/// +public class GaussDBOwnedJsonTypeMapping : JsonTypeMapping +{ + /// + /// The database type used by GaussDB ( or . + /// + public virtual GaussDBDbType GaussDBDbType { get; } + + private static readonly MethodInfo GetStringMethod + = 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), [typeof(string)])!; + + private static readonly ConstructorInfo MemoryStreamConstructor + = typeof(MemoryStream).GetConstructor([typeof(byte[])])!; + + /// + /// 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 GaussDBOwnedJsonTypeMapping(string storeType) + : base(storeType, typeof(JsonElement), dbType: null) + { + GaussDBDbType = storeType switch + { + "json" => GaussDBDbType.Json, + "jsonb" => GaussDBDbType.Jsonb, + _ => throw new ArgumentException("Only the json and jsonb types are supported", nameof(storeType)) + }; + } + + /// + /// 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 MethodInfo GetDataReaderMethod() + => GetStringMethod; + + /// + /// 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 Expression CustomizeDataReaderExpression(Expression expression) + => Expression.New( + MemoryStreamConstructor, + Expression.Call( + Expression.Property(null, UTF8Property), + EncodingGetBytesMethod, + 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 GaussDBOwnedJsonTypeMapping(RelationalTypeMappingParameters parameters, GaussDBDbType npgsqlDbType) + : base(parameters) + { + GaussDBDbType = npgsqlDbType; + } + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {nameof(GaussDBOwnedJsonTypeMapping)} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + base.ConfigureParameter(parameter); + npgsqlParameter.GaussDBDbType = GaussDBDbType; + } + + /// + /// 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 string EscapeSqlLiteral(string literal) + => literal.Replace("'", "''"); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'"; + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBOwnedJsonTypeMapping(parameters, GaussDBDbType); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBPgLsnTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBPgLsnTypeMapping.cs new file mode 100644 index 0000000000..fed29ecab4 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBPgLsnTypeMapping.cs @@ -0,0 +1,130 @@ +using System.Text; +using System.Text.Json; +using HuaweiCloud.GaussDBTypes; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for the GaussDB pg_lsn type. +/// +/// +/// See: https://www.postgresql.org/docs/current/datatype-pg-lsn.html +/// +public class GaussDBPgLsnTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBPgLsnTypeMapping Default { 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 GaussDBPgLsnTypeMapping() + : base( + "pg_lsn", typeof(GaussDBLogSequenceNumber), GaussDBDbType.PgLsn, + jsonValueReaderWriter: JsonLogSequenceNumberReaderWriter.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. + /// + protected GaussDBPgLsnTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.PgLsn) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBPgLsnTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var lsn = (GaussDBLogSequenceNumber)value; + var builder = new StringBuilder("PG_LSN '") + .Append(lsn.ToString()) + .Append('\''); + return builder.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var lsn = (GaussDBLogSequenceNumber)value; + return Expression.New(Constructor, Expression.Constant((ulong)lsn)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBLogSequenceNumber).GetConstructor([typeof(ulong)])!; + + /// + /// 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 GaussDBLogSequenceNumber FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => GaussDBLogSequenceNumber.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, GaussDBLogSequenceNumber value) + => writer.WriteStringValue(value.ToString()); + + /// + public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRangeTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRangeTypeMapping.cs new file mode 100644 index 0000000000..a546d4ef58 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRangeTypeMapping.cs @@ -0,0 +1,269 @@ +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The type mapping for GaussDB range types. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/rangetypes.html +/// +public class GaussDBRangeTypeMapping : GaussDBTypeMapping +{ + private PropertyInfo? _isEmptyProperty; + private PropertyInfo? _lowerProperty; + private PropertyInfo? _upperProperty; + private PropertyInfo? _lowerInclusiveProperty; + private PropertyInfo? _upperInclusiveProperty; + private PropertyInfo? _lowerInfiniteProperty; + private PropertyInfo? _upperInfiniteProperty; + + private ConstructorInfo? _rangeConstructor1; + private ConstructorInfo? _rangeConstructor2; + private ConstructorInfo? _rangeConstructor3; + + /// + /// 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 GaussDBRangeTypeMapping Default { get; } = new(); + + // ReSharper disable once MemberCanBePrivate.Global + /// + /// The relational type mapping of the range's subtype. + /// + public virtual RelationalTypeMapping SubtypeMapping { get; } + + /// + /// For user-defined ranges, we have no and so the PG type name is set on + /// instead. + /// + public virtual string? UnquotedStoreType { get; init; } + + /// + /// Constructs an instance of the class for a built-in range type which has a + /// defined. + /// + /// The database type to map + /// The CLR type to map. + /// The of the built-in range. + /// The type mapping for the range subtype. + public static GaussDBRangeTypeMapping CreatBuiltInRangeMapping( + string rangeStoreType, + Type rangeClrType, + GaussDBDbType rangeGaussDBDbType, + RelationalTypeMapping subtypeMapping) + => new(rangeStoreType, rangeClrType, rangeGaussDBDbType, subtypeMapping); + + /// + /// Constructs an instance of the class for a user-defined range type which doesn't have a + /// defined. + /// + /// The database type to map, quoted. + /// The database type to map, unquoted. + /// The CLR type to map. + /// The type mapping for the range subtype. + public static GaussDBRangeTypeMapping CreatUserDefinedRangeMapping( + string quotedRangeStoreType, + string unquotedRangeStoreType, + Type rangeClrType, + RelationalTypeMapping subtypeMapping) + => new(quotedRangeStoreType, rangeClrType, rangeGaussDBDbType: GaussDBDbType.Unknown, subtypeMapping) + { + UnquotedStoreType = unquotedRangeStoreType + }; + + private GaussDBRangeTypeMapping( + string rangeStoreType, + Type rangeClrType, + GaussDBDbType rangeGaussDBDbType, + RelationalTypeMapping subtypeMapping) + : base(rangeStoreType, rangeClrType, rangeGaussDBDbType) + { + SubtypeMapping = subtypeMapping; + } + + /// + /// 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 GaussDBRangeTypeMapping( + RelationalTypeMappingParameters parameters, + GaussDBDbType npgsqlDbType, + RelationalTypeMapping subtypeMapping) + : base(parameters, npgsqlDbType) + { + SubtypeMapping = subtypeMapping; + } + + // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled + // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. + private GaussDBRangeTypeMapping() + : this("int4range", typeof(GaussDBRange), GaussDBDbType.IntegerRange, subtypeMapping: null!) + { + } + + /// + /// 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 GaussDBRangeTypeMapping Clone(GaussDBDbType npgsqlDbType, RelationalTypeMapping subtypeTypeMapping) + => new(Parameters, npgsqlDbType, subtypeTypeMapping); + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBRangeTypeMapping(parameters, GaussDBDbType, SubtypeMapping); + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + // Built-in range types have an GaussDBDbType, so we just do the normal thing. + if (UnquotedStoreType is null) + { + Check.DebugAssert(GaussDBDbType is not GaussDBDbType.Unknown, "GaussDBDbType is Unknown but no PgDataTypeName is configured"); + base.ConfigureParameter(parameter); + return; + } + + Check.DebugAssert(GaussDBDbType is GaussDBDbType.Unknown, "PgDataTypeName is non-null, but GaussDBDbType is " + GaussDBDbType); + + if (parameter is not GaussDBParameter npgsqlParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {GetType().Name} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + npgsqlParameter.DataTypeName = UnquotedStoreType; + } + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"'{GenerateEmbeddedNonNullSqlLiteral(value)}'::{StoreType}"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + { + InitializeAccessors(ClrType, SubtypeMapping.ClrType); + + var builder = new StringBuilder(); + + if ((bool)_isEmptyProperty.GetValue(value)!) + { + builder.Append("empty"); + } + else + { + builder.Append((bool)_lowerInclusiveProperty.GetValue(value)! ? '[' : '('); + + if (!(bool)_lowerInfiniteProperty.GetValue(value)!) + { + builder.Append(SubtypeMapping.GenerateEmbeddedSqlLiteral(_lowerProperty.GetValue(value))); + } + + builder.Append(','); + + if (!(bool)_upperInfiniteProperty.GetValue(value)!) + { + builder.Append(SubtypeMapping.GenerateEmbeddedSqlLiteral(_upperProperty.GetValue(value))); + } + + builder.Append((bool)_upperInclusiveProperty.GetValue(value)! ? ']' : ')'); + } + + return builder.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + InitializeAccessors(ClrType, SubtypeMapping.ClrType); + + var lower = _lowerProperty.GetValue(value); + var upper = _upperProperty.GetValue(value); + var lowerInclusive = (bool)_lowerInclusiveProperty.GetValue(value)!; + var upperInclusive = (bool)_upperInclusiveProperty.GetValue(value)!; + var lowerInfinite = (bool)_lowerInfiniteProperty.GetValue(value)!; + var upperInfinite = (bool)_upperInfiniteProperty.GetValue(value)!; + + return lowerInfinite || upperInfinite + ? Expression.New( + _rangeConstructor3, + Expression.Constant(lower), + Expression.Constant(lowerInclusive), + Expression.Constant(lowerInfinite), + Expression.Constant(upper), + Expression.Constant(upperInclusive), + Expression.Constant(upperInfinite)) + : lowerInclusive && upperInclusive + ? Expression.New( + _rangeConstructor1, + Expression.Constant(lower), + Expression.Constant(upper)) + : Expression.New( + _rangeConstructor2, + Expression.Constant(lower), + Expression.Constant(lowerInclusive), + Expression.Constant(upper), + Expression.Constant(upperInclusive)); + } + + [MemberNotNull( + "_isEmptyProperty", + "_lowerProperty", "_upperProperty", + "_lowerInclusiveProperty", "_upperInclusiveProperty", + "_lowerInfiniteProperty", "_upperInfiniteProperty", + "_rangeConstructor1", "_rangeConstructor2", "_rangeConstructor3")] + private void InitializeAccessors(Type rangeClrType, Type subtypeClrType) + { + _isEmptyProperty = rangeClrType.GetProperty(nameof(GaussDBRange.IsEmpty))!; + _lowerProperty = rangeClrType.GetProperty(nameof(GaussDBRange.LowerBound))!; + _upperProperty = rangeClrType.GetProperty(nameof(GaussDBRange.UpperBound))!; + _lowerInclusiveProperty = rangeClrType.GetProperty(nameof(GaussDBRange.LowerBoundIsInclusive))!; + _upperInclusiveProperty = rangeClrType.GetProperty(nameof(GaussDBRange.UpperBoundIsInclusive))!; + _lowerInfiniteProperty = rangeClrType.GetProperty(nameof(GaussDBRange.LowerBoundInfinite))!; + _upperInfiniteProperty = rangeClrType.GetProperty(nameof(GaussDBRange.UpperBoundInfinite))!; + + _rangeConstructor1 = rangeClrType.GetConstructor( + [subtypeClrType, subtypeClrType])!; + _rangeConstructor2 = rangeClrType.GetConstructor( + [subtypeClrType, typeof(bool), subtypeClrType, typeof(bool)])!; + _rangeConstructor3 = rangeClrType.GetConstructor( + [subtypeClrType, typeof(bool), typeof(bool), subtypeClrType, typeof(bool), typeof(bool)])!; + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRegconfigTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRegconfigTypeMapping.cs new file mode 100644 index 0000000000..631455c320 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRegconfigTypeMapping.cs @@ -0,0 +1,61 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBRegconfigTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBRegconfigTypeMapping Default { 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 GaussDBRegconfigTypeMapping() + : base("regconfig", typeof(uint), GaussDBDbType.Regconfig) + { + } + + /// + /// 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 GaussDBRegconfigTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Regconfig) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBRegconfigTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"'{EscapeSqlLiteral((string)value)}'"; + + private string EscapeSqlLiteral(string literal) + => Check.NotNull(literal, nameof(literal)).Replace("'", "''"); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRegdictionaryTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRegdictionaryTypeMapping.cs new file mode 100644 index 0000000000..a608053747 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRegdictionaryTypeMapping.cs @@ -0,0 +1,61 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBRegdictionaryTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBRegdictionaryTypeMapping Default { 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 GaussDBRegdictionaryTypeMapping() + : base("regdictionary", typeof(uint), GaussDBDbType.Oid) + { + } + + /// + /// 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 GaussDBRegdictionaryTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Oid) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBRegdictionaryTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"'{EscapeSqlLiteral((string)value)}'"; + + private string EscapeSqlLiteral(string literal) + => Check.NotNull(literal, nameof(literal)).Replace("'", "''"); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRowValueTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRowValueTypeMapping.cs new file mode 100644 index 0000000000..9b265b63af --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBRowValueTypeMapping.cs @@ -0,0 +1,61 @@ +using System.Data.Common; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// TODO: Update +/// Every node in the SQL tree must have a type mapping, but row values aren't actual values (in the sense that they can be sent as +/// parameters, or have a literal representation). So we have a dummy type mapping for that. +/// +public class GaussDBRowValueTypeMapping : RelationalTypeMapping +{ + /// + /// 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 GaussDBRowValueTypeMapping(Type clrType) + : base(new RelationalTypeMappingParameters(new CoreTypeMappingParameters(clrType), storeType: "record")) + { + } + + /// + /// 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 GaussDBRowValueTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBRowValueTypeMapping(parameters); + + /// + /// 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. + /// + // SQL generation for row values is in GaussDBQuerySqlGenerator + protected override string GenerateNonNullSqlLiteral(object value) + => throw new InvalidOperationException("GenerateNonNullSqlLiteral not supported on GaussDBRowValueTypeMapping"); + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + => throw new InvalidOperationException("ConfigureParameter not supported on GaussDBRowValueTypeMapping"); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBStringTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBStringTypeMapping.cs new file mode 100644 index 0000000000..fe3b446fd3 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBStringTypeMapping.cs @@ -0,0 +1,93 @@ +using System.Data.Common; +using HuaweiCloud.GaussDB; +using HuaweiCloud.GaussDBTypes; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The base class for mapping GaussDB-specific string types. It configures parameters with the +/// provider-specific type enum. +/// +public class GaussDBStringTypeMapping : StringTypeMapping, IGaussDBTypeMapping +{ + /// + /// 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 new GaussDBStringTypeMapping Default { get; } = new("text", GaussDBDbType.Text); + + /// + /// 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 GaussDBDbType GaussDBDbType { 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 GaussDBStringTypeMapping(string storeType, GaussDBDbType gaussdbDbType) + : base(storeType, System.Data.DbType.String) + { + GaussDBDbType = gaussdbDbType; + } + + /// + /// 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 GaussDBStringTypeMapping( + RelationalTypeMappingParameters parameters, + GaussDBDbType gaussdbDbType) + : base(parameters) + { + GaussDBDbType = gaussdbDbType; + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBStringTypeMapping(parameters, GaussDBDbType); + + /// + /// 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 GaussDBStringTypeMapping Clone(GaussDBDbType gaussdbDbType) + => new(Parameters, gaussdbDbType); + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter gaussdbParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {GetType().Name} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + base.ConfigureParameter(parameter); + gaussdbParameter.GaussDBDbType = GaussDBDbType; + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTidTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTidTypeMapping.cs new file mode 100644 index 0000000000..7155e4edec --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTidTypeMapping.cs @@ -0,0 +1,83 @@ +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTidTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTidTypeMapping Default { 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 GaussDBTidTypeMapping() + : base("tid", typeof(GaussDBTid), GaussDBDbType.Tid) + { + } + + /// + /// 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 GaussDBTidTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Tid) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTidTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var tid = (GaussDBTid)value; + var builder = new StringBuilder("TID '("); + builder.Append(tid.BlockNumber); + builder.Append(','); + builder.Append(tid.OffsetNumber); + builder.Append(")'"); + return builder.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var tid = (GaussDBTid)value; + return Expression.New(Constructor, Expression.Constant(tid.BlockNumber), Expression.Constant(tid.OffsetNumber)); + } + + private static readonly ConstructorInfo Constructor = + typeof(GaussDBTid).GetConstructor([typeof(uint), typeof(ushort)])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimeTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimeTypeMapping.cs new file mode 100644 index 0000000000..db0abd6727 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimeTypeMapping.cs @@ -0,0 +1,96 @@ +using System.Globalization; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTimeTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTimeTypeMapping Default { get; } = new(typeof(TimeOnly)); + + /// + /// 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 GaussDBTimeTypeMapping(Type clrType) + : base( + "time without time zone", + clrType, + GaussDBDbType.Time, + clrType == typeof(TimeOnly) + ? JsonTimeOnlyReaderWriter.Instance + : clrType == typeof(TimeSpan) + ? JsonTimeSpanReaderWriter.Instance + : throw new ArgumentException("clrType must be TimeOnly or TimeSpan", nameof(clrType))) + { + } + + /// + /// 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 GaussDBTimeTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Time) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTimeTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null ? storeType : $"time({parameters.Precision}) without time zone"; + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"TIME '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => value switch + { + TimeSpan ts => ts.Ticks % 10000000 == 0 + ? ts.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture) + : ts.ToString(@"hh\:mm\:ss\.FFFFFF", CultureInfo.InvariantCulture), + TimeOnly t => t.Ticks % 10000000 == 0 + ? t.ToString(@"HH\:mm\:ss", CultureInfo.InvariantCulture) + : t.ToString(@"HH\:mm\:ss\.FFFFFF", CultureInfo.InvariantCulture), + _ => throw new InvalidCastException($"Can't generate a time SQL literal for CLR type {value.GetType()}") + }; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimeTzTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimeTzTypeMapping.cs new file mode 100644 index 0000000000..d0721979bb --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimeTzTypeMapping.cs @@ -0,0 +1,123 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTimeTzTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTimeTzTypeMapping Default { 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 GaussDBTimeTzTypeMapping() + : base("time with time zone", typeof(DateTimeOffset), GaussDBDbType.TimeTz, JsonTimeTzReaderWriter.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. + /// + protected GaussDBTimeTzTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.TimeTz) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTimeTzTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null ? storeType : $"time({parameters.Precision}) with time zone"; + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => FormattableString.Invariant($"TIMETZ '{(DateTimeOffset)value:HH:mm:ss.FFFFFFz}'"); + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => FormattableString.Invariant(@$"{(DateTimeOffset)value:HH:mm:ss.FFFFFFz}"); + + /// + /// 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 + /// 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 JsonTimeTzReaderWriter Instance { get; } = new(); + + 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.GaussDB/Storage/Internal/Mapping/GaussDBTimestampTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimestampTypeMapping.cs new file mode 100644 index 0000000000..c15bc5f351 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimestampTypeMapping.cs @@ -0,0 +1,160 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTimestampTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTimestampTypeMapping Default { 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 GaussDBTimestampTypeMapping() + : base("timestamp without time zone", typeof(DateTime), GaussDBDbType.Timestamp, GaussDBJsonTimestampReaderWriter.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. + /// + protected GaussDBTimestampTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Timestamp) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTimestampTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null ? storeType : $"timestamp({parameters.Precision}) without time zone"; + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"TIMESTAMP '{GenerateLiteralCore(value)}'"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => $""" + "{GenerateLiteralCore(value)}" + """; + + private string GenerateLiteralCore(object value) + => FormatDateTime((DateTime)value); + + private static string FormatDateTime(DateTime dateTime) + { + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (dateTime == DateTime.MinValue) + { + return "-infinity"; + } + + if (dateTime == DateTime.MaxValue) + { + return "infinity"; + } + } + + return GaussDBTypeMappingSource.LegacyTimestampBehavior || dateTime.Kind != DateTimeKind.Utc + ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + : throw new ArgumentException("'timestamp without time zone' literal cannot be generated for a UTC DateTime"); + } + + /// + /// 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 GaussDBJsonTimestampReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty = typeof(GaussDBJsonTimestampReaderWriter).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 GaussDBJsonTimestampReaderWriter 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()!; + + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateTime.MinValue; + case "infinity": + return DateTime.MaxValue; + } + } + + 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.GaussDB/Storage/Internal/Mapping/GaussDBTimestampTzTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimestampTzTypeMapping.cs new file mode 100644 index 0000000000..cbc7781848 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTimestampTzTypeMapping.cs @@ -0,0 +1,264 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTimestampTzTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTimestampTzTypeMapping Default { get; } = new(typeof(DateTime)); + + /// + /// 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 GaussDBTimestampTzTypeMapping(Type clrType) + : base( + "timestamp with time zone", + clrType, + GaussDBDbType.TimestampTz, + clrType == typeof(DateTime) + ? GaussDBJsonTimestampTzDateTimeReaderWriter.Instance + : clrType == typeof(DateTimeOffset) + ? GaussDBJsonTimestampTzDateTimeOffsetReaderWriter.Instance + : throw new ArgumentException("clrType must be DateTime or DateTimeOffset", nameof(clrType))) + { + } + + /// + /// 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 GaussDBTimestampTzTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.TimestampTz) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTimestampTzTypeMapping(parameters); + + /// + /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) + => parameters.Precision is null ? storeType : $"timestamp({parameters.Precision}) with time zone"; + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => $"TIMESTAMPTZ '{Format(value)}'"; + + /// + /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) + => $""" + "{Format(value)}" + """; + + private static string Format(object value) + => value switch + { + DateTime dateTime => Format(dateTime), + DateTimeOffset dateTimeOffset => Format(dateTimeOffset), + _ => throw new InvalidCastException( + $"Attempted to generate timestamptz literal for type {value.GetType()}, only DateTime and DateTimeOffset are supported") + }; + + private static string Format(DateTime dateTime) + { + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (dateTime == DateTime.MinValue) + { + return "-infinity"; + } + + if (dateTime == DateTime.MaxValue) + { + return "infinity"; + } + } + + return dateTime.Kind switch + { + DateTimeKind.Utc => dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z', + + DateTimeKind.Unspecified => GaussDBTypeMappingSource.LegacyTimestampBehavior || dateTime == default + ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z' + : throw new ArgumentException( + $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), + + DateTimeKind.Local => GaussDBTypeMappingSource.LegacyTimestampBehavior + ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture) + : throw new ArgumentException( + $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), + + _ => throw new UnreachableException() + }; + } + + private static string Format(DateTimeOffset dateTimeOffset) + { + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (dateTimeOffset == DateTimeOffset.MinValue) + { + return "-infinity"; + } + + if (dateTimeOffset == DateTimeOffset.MaxValue) + { + return "infinity"; + } + } + + return dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", 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 sealed class GaussDBJsonTimestampTzDateTimeReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty + = typeof(GaussDBJsonTimestampTzDateTimeReaderWriter).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 GaussDBJsonTimestampTzDateTimeReaderWriter 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()!; + + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateTime.MinValue; + case "infinity": + return DateTime.MaxValue; + } + } + + // Our JSON string representation ends with Z (UTC), but DateTime.Parse returns a Local timestamp even in that case. Convert + // it in order to return a DateTime with Kind UTC. + 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); + } + + /// + /// 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 GaussDBJsonTimestampTzDateTimeOffsetReaderWriter : JsonValueReaderWriter + { + private static readonly PropertyInfo InstanceProperty + = typeof(GaussDBJsonTimestampTzDateTimeOffsetReaderWriter).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 GaussDBJsonTimestampTzDateTimeOffsetReaderWriter 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()!; + + if (!GaussDBTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateTimeOffset.MinValue; + case "infinity": + return DateTimeOffset.MaxValue; + } + } + + 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.GaussDB/Storage/Internal/Mapping/GaussDBTsQueryTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsQueryTypeMapping.cs new file mode 100644 index 0000000000..9db491df0d --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsQueryTypeMapping.cs @@ -0,0 +1,71 @@ +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTsQueryTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTsQueryTypeMapping Default { 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 GaussDBTsQueryTypeMapping() + : base("tsquery", typeof(GaussDBTsQuery), GaussDBDbType.TsQuery) + { + } + + /// + /// 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 GaussDBTsQueryTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.TsQuery) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTsQueryTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + Check.NotNull(value, nameof(value)); + var query = (GaussDBTsQuery)value; + var builder = new StringBuilder(); + builder.Append("TSQUERY "); + var indexOfFirstQuote = builder.Length - 1; + query.Write(builder); + builder.Replace("'", "''"); + builder[indexOfFirstQuote] = '\''; + builder.Append("'"); + return builder.ToString(); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsRankingNormalizationTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsRankingNormalizationTypeMapping.cs new file mode 100644 index 0000000000..43ad409304 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsRankingNormalizationTypeMapping.cs @@ -0,0 +1,53 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTsRankingNormalizationTypeMapping : IntTypeMapping +{ + /// + /// 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 new GaussDBTsRankingNormalizationTypeMapping Default { 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 GaussDBTsRankingNormalizationTypeMapping() + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(GaussDBTsRankingNormalization), new EnumToNumberConverter()), + "integer")) + { + } + + /// + /// 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 GaussDBTsRankingNormalizationTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTsRankingNormalizationTypeMapping(parameters); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsVectorMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsVectorMapping.cs new file mode 100644 index 0000000000..0ec1ea0a28 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTsVectorMapping.cs @@ -0,0 +1,71 @@ +using System.Text; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBTsVectorTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBTsVectorTypeMapping Default { 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 GaussDBTsVectorTypeMapping() + : base("tsvector", typeof(GaussDBTsVector), GaussDBDbType.TsVector) + { + } + + /// + /// 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 GaussDBTsVectorTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.TsVector) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBTsVectorTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + Check.NotNull(value, nameof(value)); + var vector = (GaussDBTsVector)value; + var builder = new StringBuilder(); + builder.Append("TSVECTOR "); + var indexOfFirstQuote = builder.Length - 1; + builder.Append(vector); + builder.Replace("'", "''"); + builder[indexOfFirstQuote] = '\''; + builder.Append("'"); + return builder.ToString(); + } +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTypeMapping.cs new file mode 100644 index 0000000000..bdc60d1795 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBTypeMapping.cs @@ -0,0 +1,105 @@ +using System.Data.Common; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +/// +/// The base class for mapping GaussDB-specific types. It configures parameters with the +/// provider-specific type enum. +/// +public abstract class GaussDBTypeMapping : RelationalTypeMapping, IGaussDBTypeMapping +{ + /// + public virtual GaussDBDbType GaussDBDbType { get; } + + // ReSharper disable once PublicConstructorInAbstractClass + /// + /// Constructs an instance of the class. + /// + /// The database type to map. + /// The CLR type to map. + /// The database type used by GaussDB. + /// Handles reading and writing JSON values for instances of the mapped type. + public GaussDBTypeMapping(string storeType, Type clrType, GaussDBDbType gaussdbDbType, JsonValueReaderWriter? jsonValueReaderWriter = null) + : base(storeType, clrType, jsonValueReaderWriter: jsonValueReaderWriter) + { + GaussDBDbType = gaussdbDbType; + } + + /// + /// Constructs an instance of the class. + /// + /// The parameters for this mapping. + /// The database type of the range subtype. + protected GaussDBTypeMapping(RelationalTypeMappingParameters parameters, GaussDBDbType gaussdbDbType) + : base(parameters) + { + GaussDBDbType = gaussdbDbType; + } + + /// + /// 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 void ConfigureParameter(DbParameter parameter) + { + if (parameter is not GaussDBParameter gaussdbParameter) + { + throw new InvalidOperationException( + $"GaussDB-specific type mapping {GetType().Name} being used with non-GaussDB parameter type {parameter.GetType().Name}"); + } + + base.ConfigureParameter(parameter); + gaussdbParameter.GaussDBDbType = GaussDBDbType; + } + + /// + /// Generates the SQL representation of a literal value meant to be embedded in another literal value, e.g. in a range. + /// + /// The literal value. + /// + /// The generated string. + /// + public virtual string GenerateEmbeddedSqlLiteral(object? value) + { + value = ConvertUnderlyingEnumValueToEnum(value); + + if (Converter != null) + { + value = Converter.ConvertToProvider(value); + } + + return GenerateEmbeddedProviderValueSqlLiteral(value); + } + + /// + /// Generates the SQL representation of a literal value without conversion, meant to be embedded in another literal value, + /// e.g. in a range. + /// + /// The literal value. + /// + /// The generated string. + /// + public virtual string GenerateEmbeddedProviderValueSqlLiteral(object? value) + => value == null + ? "NULL" + : GenerateEmbeddedNonNullSqlLiteral(value); + + /// + /// Generates the SQL representation of a non-null literal value, meant to be embedded in another literal value, e.g. in a range. + /// + /// The literal value. + /// + /// The generated string. + /// + protected virtual string GenerateEmbeddedNonNullSqlLiteral(object value) + => GenerateNonNullSqlLiteral(value); + + // Copied from RelationalTypeMapping + private object? ConvertUnderlyingEnumValueToEnum(object? value) + => value?.GetType().IsInteger() == true && ClrType.UnwrapNullableType().IsEnum + ? Enum.ToObject(ClrType.UnwrapNullableType(), value) + : value; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBUIntTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBUIntTypeMapping.cs new file mode 100644 index 0000000000..e07f5f3102 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBUIntTypeMapping.cs @@ -0,0 +1,52 @@ +using HuaweiCloud.GaussDBTypes; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBUIntTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBUIntTypeMapping Default { get; } = new("xid", GaussDBDbType.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 + /// 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 GaussDBUIntTypeMapping(string storeType, GaussDBDbType npgsqlDbType) + : base(storeType, typeof(uint), npgsqlDbType, JsonUInt32ReaderWriter.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. + /// + protected GaussDBUIntTypeMapping(RelationalTypeMappingParameters parameters, GaussDBDbType npgsqlDbType) + : base(parameters, npgsqlDbType) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBUIntTypeMapping(parameters, GaussDBDbType); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBULongTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBULongTypeMapping.cs new file mode 100644 index 0000000000..d96d4cd413 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBULongTypeMapping.cs @@ -0,0 +1,52 @@ +using HuaweiCloud.GaussDBTypes; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBULongTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBULongTypeMapping Default { get; } = new("xid8", GaussDBDbType.Xid8); + + /// + /// 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 GaussDBULongTypeMapping(string storeType, GaussDBDbType npgsqlDbType) + : base(storeType, typeof(ulong), npgsqlDbType, JsonUInt64ReaderWriter.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. + /// + protected GaussDBULongTypeMapping(RelationalTypeMappingParameters parameters, GaussDBDbType npgsqlDbType) + : base(parameters, npgsqlDbType) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBULongTypeMapping(parameters, GaussDBDbType); +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBVarbitTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBVarbitTypeMapping.cs new file mode 100644 index 0000000000..82bdc6a0ca --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/GaussDBVarbitTypeMapping.cs @@ -0,0 +1,96 @@ +using System.Collections; +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Json; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 GaussDBVarbitTypeMapping : GaussDBTypeMapping +{ + /// + /// 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 GaussDBVarbitTypeMapping Default { 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 GaussDBVarbitTypeMapping() + : base("bit varying", typeof(BitArray), GaussDBDbType.Varbit, jsonValueReaderWriter: JsonBitArrayReaderWriter.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. + /// + protected GaussDBVarbitTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, GaussDBDbType.Varbit) + { + } + + /// + /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new GaussDBVarbitTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + { + var bits = (BitArray)value; + var sb = new StringBuilder(); + sb.Append("B'"); + for (var i = 0; i < bits.Count; i++) + { + sb.Append(bits[i] ? '1' : '0'); + } + + sb.Append('\''); + return sb.ToString(); + } + + /// + /// 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 Expression GenerateCodeLiteral(object value) + { + var bits = (BitArray)value; + var exprs = new Expression[bits.Count]; + for (var i = 0; i < bits.Count; i++) + { + exprs[i] = Expression.Constant(bits[i]); + } + + return Expression.New( + Constructor, + Expression.NewArrayInit(typeof(bool), exprs)); + } + + private static readonly ConstructorInfo Constructor = + typeof(BitArray).GetConstructor([typeof(bool[])])!; +} diff --git a/src/EFCore.GaussDB/Storage/Internal/Mapping/IGaussDBTypeMapping.cs b/src/EFCore.GaussDB/Storage/Internal/Mapping/IGaussDBTypeMapping.cs new file mode 100644 index 0000000000..01f88bdf02 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/Internal/Mapping/IGaussDBTypeMapping.cs @@ -0,0 +1,15 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.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 +/// 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 interface IGaussDBTypeMapping +{ + /// + /// The database type used by GaussDB. + /// + GaussDBDbType GaussDBDbType { get; } +} diff --git a/src/EFCore.GaussDB/Storage/ValueConversion/GaussDBArrayConverter.cs b/src/EFCore.GaussDB/Storage/ValueConversion/GaussDBArrayConverter.cs new file mode 100644 index 0000000000..97401dd9c7 --- /dev/null +++ b/src/EFCore.GaussDB/Storage/ValueConversion/GaussDBArrayConverter.cs @@ -0,0 +1,333 @@ +using System.Collections; +using static System.Linq.Expressions.Expression; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.ValueConversion; + +/// +/// A value converter that can convert between array types; accepts an optional for the element, but can be +/// used without one to convert e.g. from a list to an array. +/// +public class GaussDBArrayConverter + : ValueConverter + where TModelCollection : IEnumerable + where TConcreteModelCollection : IEnumerable + where TProviderCollection : IEnumerable +{ + /// + /// The value converter for the element type of the array. + /// + public virtual ValueConverter? ElementConverter { get; } + + /// + /// Constructs a new instance of . + /// + public GaussDBArrayConverter() + : this(elementConverter: null) + { + } + + /// + /// Constructs a new instance of . + /// + public GaussDBArrayConverter(ValueConverter? elementConverter) + : base( + // We assume that TProviderCollection is always a concrete, instantiable type (in fact it's always an array over the element) + ArrayConversionExpression( + elementConverter?.ConvertToProviderExpression), + ArrayConversionExpression( + elementConverter?.ConvertFromProviderExpression)) + { + var modelElementType = typeof(TModelCollection).TryGetElementType(typeof(IEnumerable<>)); + var providerElementType = typeof(TProviderCollection).TryGetElementType(typeof(IEnumerable<>)); + if (modelElementType is null || providerElementType is null) + { + throw new ArgumentException("Can only convert between arrays"); + } + + if (elementConverter is not null) + { + if (modelElementType.UnwrapNullableType() != elementConverter.ModelClrType.UnwrapNullableType()) + { + throw new ArgumentException( + $"The element's value converter model type ({elementConverter.ModelClrType}), doesn't match the array's ({modelElementType})"); + } + + if (providerElementType.UnwrapNullableType() != elementConverter.ProviderClrType.UnwrapNullableType()) + { + throw new ArgumentException( + $"The element's value converter provider type ({elementConverter.ProviderClrType}), doesn't match the array's ({providerElementType})"); + } + } + + ElementConverter = elementConverter; + } + + /// + /// Generates a lambda expression that accepts an array, and converts it to another array by looping and applying + /// a conversion lambda to each of its elements. + /// + private static Expression> ArrayConversionExpression( + LambdaExpression? elementConversionExpression) + { + var inputElementType = typeof(TInput).IsArray + ? typeof(TInput).GetElementType() + : typeof(TInput).TryGetElementType(typeof(IEnumerable<>)); + + var outputElementType = typeof(TOutput).IsArray + ? typeof(TOutput).GetElementType() + : typeof(TOutput).TryGetElementType(typeof(IEnumerable<>)); + + if (inputElementType is null || outputElementType is null) + { + throw new ArgumentException("Both TInput and TOutput must be arrays or IList"); + } + + // elementConversionExpression is always over non-nullable value types. If the array is over nullable types, + // we need to sanitize via an external null check. + if (elementConversionExpression is not null && inputElementType.IsNullableType() && outputElementType.IsNullableType()) + { + // p => p is null ? null : elementConversionExpression(p) + var p = Parameter(inputElementType, "foo"); + elementConversionExpression = Lambda( + Condition( + Equal(p, Constant(null, inputElementType)), + Constant(null, outputElementType), + Convert( + Invoke( + elementConversionExpression, + // The user-provided conversion lambda typically accepts non-nullable (value) types, with EF Core doing the + // null-sanitization and conversion to non-nullable; do this here unless the user-provided lambda happens to + // accept a nullable value type parameter. + elementConversionExpression.Parameters[0].Type.IsNullableType() + ? p + : Convert(p, inputElementType.UnwrapNullableType())), + outputElementType)), + p); + } + + var input = Parameter(typeof(TInput), "input"); + var convertedInput = input; + var output = Parameter(typeof(TConcreteOutput), "result"); + var lengthVariable = Variable(typeof(int), "length"); + + var expressions = new List(); + var variables = new List { output, lengthVariable }; + + Expression getInputLength; + Func? indexer; + + // 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 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; + + // 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; + } + + // 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; + } + + // 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( + [ + // 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) switch + { + var t when t.IsArray => NewArrayBounds(outputElementType, lengthVariable), + var t when typeof(TConcreteOutput).GetConstructor([typeof(int)]) is ConstructorInfo ctorWithLength => New(ctorWithLength, lengthVariable), + _ => New(typeof(TConcreteOutput)) + }) + ]); + + 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: counter, + initValue: Constant(0), + condition: LessThan(counter, lengthVariable), + increment: AddAssign(counter, Constant(1)), + loopContent: + typeof(TConcreteOutput).IsArray + ? Assign( + ArrayAccess(output, counter), + elementConversionExpression is null + ? indexer(counter) + : Invoke(elementConversionExpression, indexer(counter))) + : Call( + output, + typeof(TConcreteOutput).GetMethod("Add", [outputElementType])!, + elementConversionExpression is null + ? 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); + + Expression body = Block(typeof(TOutput), variables, expressions); + + // If the input type is a reference type, first check if the input array is null and return null + // (or default for output value type) if so, bypassing all of the logic above. + if (!typeof(TInput).IsValueType) + { + body = Condition( + ReferenceEqual(input, Constant(null, typeof(TInput))), + typeof(TOutput).IsValueType + ? New(typeof(TConcreteOutput)) + : Constant(null, typeof(TOutput)), + body); + } + + return Lambda>(body, input); + } + + private static Expression ForLoop( + ParameterExpression loopVar, + Expression initValue, + Expression condition, + Expression increment, + Expression loopContent) + { + var initAssign = Assign(loopVar, initValue); + var breakLabel = Label("LoopBreak"); + var loop = Block( + [loopVar], + initAssign, + Loop( + IfThenElse( + condition, + Block( + loopContent, + increment + ), + Break(breakLabel) + ), + breakLabel) + ); + + return loop; + } +} diff --git a/src/EFCore.PG/Strings.resx b/src/EFCore.GaussDB/Strings.resx similarity index 100% rename from src/EFCore.PG/Strings.resx rename to src/EFCore.GaussDB/Strings.resx diff --git a/src/EFCore.GaussDB/Types/GaussDBTsRankingNormalization.cs b/src/EFCore.GaussDB/Types/GaussDBTsRankingNormalization.cs new file mode 100644 index 0000000000..0cd98991d8 --- /dev/null +++ b/src/EFCore.GaussDB/Types/GaussDBTsRankingNormalization.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Specifies whether and how a document's length should impact its rank. +/// This is used with the ranking functions in . +/// See http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING +/// for more information about the behaviors that are controlled by this value. +/// +[Flags] +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public enum GaussDBTsRankingNormalization +{ + /// + /// Ignores the document length. + /// + Default = 0, + + /// + /// Divides the rank by 1 + the logarithm of the document length. + /// + DivideBy1PlusLogLength = 1, + + /// + /// Divides the rank by the document length. + /// + DivideByLength = 2, + + /// + /// Divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd). + /// + DivideByMeanHarmonicDistanceBetweenExtents = 4, + + /// + /// Divides the rank by the number of unique words in document. + /// + DivideByUniqueWordCount = 8, + + /// + /// Divides the rank by 1 + the logarithm of the number of unique words in document. + /// + DividesBy1PlusLogUniqueWordCount = 16, + + /// + /// Divides the rank by itself + 1. + /// + DivideByItselfPlusOne = 32 +} diff --git a/src/EFCore.PG/Types/LTree.cs b/src/EFCore.GaussDB/Types/LTree.cs similarity index 98% rename from src/EFCore.PG/Types/LTree.cs rename to src/EFCore.GaussDB/Types/LTree.cs index 90b01369d9..620292f23b 100644 --- a/src/EFCore.PG/Types/LTree.cs +++ b/src/EFCore.GaussDB/Types/LTree.cs @@ -3,7 +3,7 @@ namespace Microsoft.EntityFrameworkCore; /// -/// Represents a PostgreSQL ltree type. This type is implicitly convertible to a .NET . +/// Represents a GaussDB ltree type. This type is implicitly convertible to a .NET . /// /// See https://www.postgresql.org/docs/current/ltree.html public readonly struct LTree : IEquatable diff --git a/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommand.cs b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommand.cs new file mode 100644 index 0000000000..9de298514a --- /dev/null +++ b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommand.cs @@ -0,0 +1,106 @@ +using System.Data; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Update.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 GaussDBModificationCommand : ModificationCommand +{ + private readonly bool _detailedErrorsEnabled; + + /// + /// 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 GaussDBModificationCommand(in ModificationCommandParameters modificationCommandParameters) + : base(in modificationCommandParameters) + { + _detailedErrorsEnabled = modificationCommandParameters.DetailedErrorsEnabled; + } + + /// + /// 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 GaussDBModificationCommand(in NonTrackedModificationCommandParameters modificationCommandParameters) + : base(in modificationCommandParameters) + { + } + + /// + /// 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 void ProcessSinglePropertyJsonUpdate(ref ColumnModificationParameters parameters) + { + // PG jsonb_set accepts a jsonb parameter for the value to be set - not an int, boolean or string like many other providers. + // So we always pass the value through the mapping's ToJsonString() (except for null). + var mapping = parameters.Property!.GetRelationalTypeMapping(); + var value = parameters.Value; + + value = value is null + ? "null" + : (mapping.JsonValueReaderWriter?.ToJsonString(value) + ?? (mapping.Converter == null ? value : mapping.Converter.ConvertToProvider(value))); + + parameters = parameters with { Value = value }; + } + + /// + /// 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 PropagateResults(RelationalDataReader relationalReader) + { + // The default implementation of PropagateResults skips (output) parameters, since for e.g. SQL Server these aren't yet populated + // when consuming the result set (propagating output columns is done later, after the reader is closed). + // However, in GaussDB, output parameters actually get returned as the result set, so we override and take care of that here. + var columnCount = ColumnModifications.Count; + + var readerIndex = -1; + + for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) + { + var columnModification = ColumnModifications[columnIndex]; + + switch (columnModification.Column) + { + case IColumn when columnModification.IsRead: + case IStoreStoredProcedureParameter { Direction: ParameterDirection.Output or ParameterDirection.InputOutput }: + readerIndex++; + break; + + case IColumn: + case IStoreStoredProcedureParameter: + case null when columnModification.JsonPath is not null: + continue; + + default: + throw new ArgumentOutOfRangeException(); + } + + // For regular result sets, results are always propagated back into entity properties. + // But with stored procedures, there may be a rows affected result column (generated by an output parameter definition). + // Skip these. + if (columnModification.Property is null || !columnModification.IsRead) + { + continue; + } + + columnModification.Value = + columnModification.Property.GetReaderFieldValue(relationalReader, readerIndex, _detailedErrorsEnabled); + } + } +} diff --git a/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandBatch.cs b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandBatch.cs new file mode 100644 index 0000000000..aad06ee016 --- /dev/null +++ b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandBatch.cs @@ -0,0 +1,300 @@ +using System.Data; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Update.Internal; + +/// +/// The GaussDB-specific implementation for . +/// +/// +/// The usual ModificationCommandBatch implementation is , +/// which selects the number of rows modified via a SQL query. +/// GaussDB actually has no way of selecting the modified row count. +/// SQL defines GET DIAGNOSTICS which should provide this, but in GaussDB it's only available +/// in PL/pgSQL. See http://www.postgresql.org/docs/9.4/static/unsupported-features-sql-standard.html, +/// identifier F121-01. +/// Instead, the affected row count can be accessed in the GaussDB protocol itself, which seems +/// cleaner and more efficient anyway (no additional query). +/// +public class GaussDBModificationCommandBatch : ReaderModificationCommandBatch +{ + /// + /// Constructs an instance of the class. + /// + public GaussDBModificationCommandBatch( + ModificationCommandBatchFactoryDependencies dependencies, + int maxBatchSize) + : base(dependencies) + { + MaxBatchSize = maxBatchSize; + } + + /// + /// The maximum number of instances that can be added to a single batch; defaults to 1000. + /// + protected override int MaxBatchSize { 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. + /// + protected override void AddParameter(IColumnModification columnModification) + { + // GaussDB stored procedures cannot return a regular result set, and output parameter values are simply sent back as the + // result set; this is very different from SQL Server, where output parameter values can be sent back in addition to result + // sets. So we avoid adding GaussDBParameters for output parameters - we'll just retrieve and propagate the values below when + // consuming the result set. + if (columnModification.Column is IStoreStoredProcedureParameter { Direction: ParameterDirection.Output }) + { + return; + } + + base.AddParameter(columnModification); + } + + /// + /// 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 void Consume(RelationalDataReader reader) + => Consume(reader, async: false).GetAwaiter().GetResult(); + + /// + /// 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 Task ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken = default) + => Consume(reader, async: true, cancellationToken); + + private async Task Consume(RelationalDataReader reader, bool async, CancellationToken cancellationToken = default) + { + var npgsqlReader = (GaussDBDataReader)reader.DbDataReader; + +#pragma warning disable 618 + Debug.Assert( + npgsqlReader.Statements.Count == ModificationCommands.Count, + $"Reader has {npgsqlReader.Statements.Count} statements, expected {ModificationCommands.Count}"); +#pragma warning restore 618 + + var commandIndex = 0; + + try + { + bool? onResultSet = null; + while (commandIndex < ModificationCommands.Count) + { + var command = ModificationCommands[commandIndex]; + + // Note that in the PG provider, we never transmit rows affected via the result set (except with stored procedures); + // it's transmitted separately via the PG wire protocol and exposed on the reader (see below). + // As a result, if there's a result set we know that it contains values to be propagated back into the entity instance. + if (ResultSetMappings[commandIndex].HasFlag(ResultSetMapping.HasResultRow)) + { + if (async) + { + if (!(await reader.ReadAsync(cancellationToken).ConfigureAwait(false))) + { + await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, cancellationToken) + .ConfigureAwait(false); + } + } + else + { + if (!reader.Read()) + { + ThrowAggregateUpdateConcurrencyException(reader, commandIndex, 1, 0); + } + } + + if (command.RowsAffectedColumn is { } rowsAffectedColumn) + { + // Only stored procedures have rows affected, as an output parameter. For regular commands, the rows affected gets + // transferred via the Post + Debug.Assert(command.StoreStoredProcedure is not null); + + var rowsAffectedParameter = (IStoreStoredProcedureParameter)rowsAffectedColumn; + Debug.Assert(rowsAffectedParameter.Direction == ParameterDirection.Output); + + var readerIndex = -1; + + for (var i = 0; i < command.ColumnModifications.Count; i++) + { + var columnModification = command.ColumnModifications[i]; + if (columnModification.Column is IStoreStoredProcedureParameter + { + Direction: ParameterDirection.Output or ParameterDirection.InputOutput + }) + { + readerIndex++; + } + + if (columnModification.Column == rowsAffectedColumn) + { + break; + } + } + + if (reader.DbDataReader.GetInt32(readerIndex) != 1) + { + await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, cancellationToken) + .ConfigureAwait(false); + } + } + + command.PropagateResults(reader); + + commandIndex++; + + onResultSet = async + ? await npgsqlReader.NextResultAsync(cancellationToken).ConfigureAwait(false) + : npgsqlReader.NextResult(); + } + else + { + Debug.Assert(ResultSetMappings[commandIndex] == ResultSetMapping.NoResults); + + // With stored procedures, either the rows affected is returned via an output parameter (and is therefore a result + // column and handled above), or there's no rows affected and we skip the check entirely. + // Without stored procedures, PG transmits the rows affected for each statement in the CommandComplete message of the + // protocol, and we always just check that. + // TODO: when EF Core adds support for DbBatch (https://github.com/dotnet/efcore/issues/18990), we can start using that + // standardized API for fetching the rows affected by an individual command in a batch. +#pragma warning disable 618 + if (npgsqlReader.Statements[commandIndex].Rows != 1 && command.StoreStoredProcedure is null) + { + if (async) + { + await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, cancellationToken) + .ConfigureAwait(false); + } + else + { + ThrowAggregateUpdateConcurrencyException(reader, commandIndex, 1, 0); + } + } +#pragma warning restore 618 + commandIndex++; + } + } + + if (onResultSet == true) + { + Dependencies.UpdateLogger.UnexpectedTrailingResultSetWhenSaving(); + } + } + 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, + ModificationCommands[commandIndex].Entries); + } + } + + private IReadOnlyList AggregateEntries(int endIndex, int commandCount) + { + var entries = new List(); + for (var i = endIndex - commandCount; i < endIndex; i++) + { + entries.AddRange(ModificationCommands[i].Entries); + } + + return entries; + } + + /// + /// Throws an exception indicating the command affected an unexpected number of rows. + /// + /// The data reader. + /// The ordinal of the command. + /// The expected number of rows affected. + /// The actual number of rows affected. + protected virtual void ThrowAggregateUpdateConcurrencyException( + RelationalDataReader reader, + int commandIndex, + int expectedRowsAffected, + int rowsAffected) + { + var entries = AggregateEntries(commandIndex + 1, expectedRowsAffected); + var exception = new DbUpdateConcurrencyException( + RelationalStrings.UpdateConcurrencyException(expectedRowsAffected, rowsAffected), + entries); + + if (!Dependencies.UpdateLogger.OptimisticConcurrencyException( + Dependencies.CurrentContext.Context, + entries, + exception, + (c, ex, e, d) => CreateConcurrencyExceptionEventData(c, reader, ex, e, d)).IsSuppressed) + { + throw exception; + } + } + + /// + /// Throws an exception indicating the command affected an unexpected number of rows. + /// + /// The data reader. + /// The ordinal of the command. + /// The expected number of rows affected. + /// The actual number of rows affected. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + /// If the is canceled. + protected virtual async Task ThrowAggregateUpdateConcurrencyExceptionAsync( + RelationalDataReader reader, + int commandIndex, + int expectedRowsAffected, + int rowsAffected, + CancellationToken cancellationToken) + { + var entries = AggregateEntries(commandIndex + 1, expectedRowsAffected); + var exception = new DbUpdateConcurrencyException( + RelationalStrings.UpdateConcurrencyException(expectedRowsAffected, rowsAffected), + entries); + + if (!(await Dependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync( + Dependencies.CurrentContext.Context, + entries, + exception, + (c, ex, e, d) => CreateConcurrencyExceptionEventData(c, reader, ex, e, d), + cancellationToken: cancellationToken) + .ConfigureAwait(false)).IsSuppressed) + { + throw exception; + } + } + + private static RelationalConcurrencyExceptionEventData CreateConcurrencyExceptionEventData( + DbContext context, + RelationalDataReader reader, + DbUpdateConcurrencyException exception, + IReadOnlyList entries, + EventDefinition definition) + => new( + definition, + (definition1, payload) + => ((EventDefinition)definition1).GenerateMessage(((ConcurrencyExceptionEventData)payload).Exception), + context, + reader.RelationalConnection.DbConnection, + reader.DbCommand, + reader.DbDataReader, + reader.CommandId, + reader.RelationalConnection.ConnectionId, + entries, + exception); +} diff --git a/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandBatchFactory.cs b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandBatchFactory.cs new file mode 100644 index 0000000000..939e906c43 --- /dev/null +++ b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandBatchFactory.cs @@ -0,0 +1,48 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Update.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 GaussDBModificationCommandBatchFactory : IModificationCommandBatchFactory +{ + private const int DefaultMaxBatchSize = 1000; + + private readonly ModificationCommandBatchFactoryDependencies _dependencies; + private readonly int _maxBatchSize; + + /// + /// 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 GaussDBModificationCommandBatchFactory(ModificationCommandBatchFactoryDependencies dependencies, IDbContextOptions options) + { + Check.NotNull(dependencies, nameof(dependencies)); + Check.NotNull(options, nameof(options)); + + _dependencies = dependencies; + + _maxBatchSize = options.FindExtension()?.MaxBatchSize ?? DefaultMaxBatchSize; + + if (_maxBatchSize <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(RelationalOptionsExtension.MaxBatchSize), RelationalStrings.InvalidMaxBatchSize(_maxBatchSize)); + } + } + + /// + /// 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 ModificationCommandBatch Create() + => new GaussDBModificationCommandBatch(_dependencies, _maxBatchSize); +} diff --git a/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandFactory.cs b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandFactory.cs new file mode 100644 index 0000000000..ba7bb00074 --- /dev/null +++ b/src/EFCore.GaussDB/Update/Internal/GaussDBModificationCommandFactory.cs @@ -0,0 +1,30 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Update.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 GaussDBModificationCommandFactory : IModificationCommandFactory +{ + /// + /// 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 IModificationCommand CreateModificationCommand( + in ModificationCommandParameters modificationCommandParameters) + => new GaussDBModificationCommand(modificationCommandParameters); + + /// + /// 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 INonTrackedModificationCommand CreateNonTrackedModificationCommand( + in NonTrackedModificationCommandParameters modificationCommandParameters) + => new GaussDBModificationCommand(modificationCommandParameters); +} diff --git a/src/EFCore.GaussDB/Update/Internal/GaussDBUpdateSqlGenerator.cs b/src/EFCore.GaussDB/Update/Internal/GaussDBUpdateSqlGenerator.cs new file mode 100644 index 0000000000..288b0978db --- /dev/null +++ b/src/EFCore.GaussDB/Update/Internal/GaussDBUpdateSqlGenerator.cs @@ -0,0 +1,334 @@ +using System.Data; +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Update.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 GaussDBUpdateSqlGenerator : UpdateSqlGenerator +{ + /// + /// 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 GaussDBUpdateSqlGenerator(UpdateSqlGeneratorDependencies dependencies) + : base(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 + /// 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 ResultSetMapping AppendInsertOperation( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + out bool requiresTransaction) + => AppendInsertOperation(commandStringBuilder, command, commandPosition, overridingSystemValue: false, out requiresTransaction); + + /// + /// 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 ResultSetMapping AppendInsertOperation( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + bool overridingSystemValue, + out bool requiresTransaction) + { + var name = command.TableName; + var schema = command.Schema; + var operations = command.ColumnModifications; + + var writeOperations = operations.Where(o => o.IsWrite).ToList(); + var readOperations = operations.Where(o => o.IsRead).ToList(); + + AppendInsertCommand(commandStringBuilder, name, schema, writeOperations, readOperations, overridingSystemValue); + + requiresTransaction = false; + + return readOperations.Count > 0 ? ResultSetMapping.LastInResultSet : ResultSetMapping.NoResults; + } + + /// + /// 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 void AppendInsertCommand( + StringBuilder commandStringBuilder, + string name, + string? schema, + IReadOnlyList writeOperations, + IReadOnlyList readOperations, + bool overridingSystemValue) + { + AppendInsertCommandHeader(commandStringBuilder, name, schema, writeOperations); + + if (overridingSystemValue) + { + commandStringBuilder.AppendLine().Append("OVERRIDING SYSTEM VALUE"); + } + + AppendValuesHeader(commandStringBuilder, writeOperations); + AppendValues(commandStringBuilder, name, schema, writeOperations); + AppendReturningClause(commandStringBuilder, readOperations); + commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); + } + + /// + /// 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 ResultSetMapping AppendUpdateOperation( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + out bool requiresTransaction) + { + // The default implementation adds RETURNING 1 to do concurrency check (was the row actually updated), but in GaussDB we check + // the per-statement row-affected value exposed by GaussDB in the batch; so no need for RETURNING 1. + var name = command.TableName; + var schema = command.Schema; + var operations = command.ColumnModifications; + + var writeOperations = operations.Where(o => o.IsWrite).ToList(); + var conditionOperations = operations.Where(o => o.IsCondition).ToList(); + var readOperations = operations.Where(o => o.IsRead).ToList(); + + requiresTransaction = false; + + AppendUpdateCommand(commandStringBuilder, name, schema, writeOperations, readOperations, conditionOperations); + + return readOperations.Count > 0 ? ResultSetMapping.LastInResultSet : ResultSetMapping.NoResults; + } + + /// + protected override void AppendUpdateColumnValue( + ISqlGenerationHelper updateSqlGeneratorHelper, + IColumnModification columnModification, + StringBuilder stringBuilder, + string name, + string? schema) + { + if (columnModification.JsonPath is not (null or "$")) + { + Check.DebugAssert( + columnModification.TypeMapping is GaussDBOwnedJsonTypeMapping, + "ColumnModification with JsonPath but non-GaussDBOwnedJsonTypeMapping"); + + if (columnModification.TypeMapping.StoreType is "json") + { + throw new NotSupportedException( + "Cannot perform partial update because the GaussDB 'json' type has no json_set method. Use 'jsonb' instead."); + } + + Check.DebugAssert(columnModification.TypeMapping.StoreType is "jsonb", "Non-jsonb type mapping in JSON partial update"); + + // TODO: Lax or not? + stringBuilder + .Append("jsonb_set(") + .Append(updateSqlGeneratorHelper.DelimitIdentifier(columnModification.ColumnName)) + .Append(", '{"); + + // TODO: Unfortunately JsonPath is provided as a JSONPATH string, but PG's jsonb_set requires the path as an array. + // Parse the components back out (https://github.com/dotnet/efcore/issues/32185) + var components = columnModification.JsonPath.Split("."); + var needsComma = false; + for (var i = 0; i < components.Length; i++) + { + if (needsComma) + { + stringBuilder.Append(','); + } + + var component = components[i]; + var bracketOpen = component.IndexOf('['); + if (bracketOpen == -1) + { + if (i > 0) // The first component is $, representing the root + { + stringBuilder.Append(component); + needsComma = true; + } + + continue; + } + + var propertyName = component[..bracketOpen]; + if (i > 0) // The first component is $, representing the root + { + stringBuilder + .Append(propertyName) + .Append(','); + } + + stringBuilder.Append(component[(bracketOpen + 1)..^1]); + needsComma = true; + } + + stringBuilder.Append("}', "); + + // TODO: Hack around + if (columnModification.Value is null) + { + _columnModificationValueField ??= typeof(ColumnModification).GetField( + "_value", BindingFlags.Instance | BindingFlags.NonPublic)!; + _columnModificationValueField.SetValue(columnModification, "null"); + } + + base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); + + stringBuilder.Append(")"); + } + else + { + base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); + } + } + + private FieldInfo? _columnModificationValueField; + + /// + /// 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 ResultSetMapping AppendDeleteOperation( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + out bool requiresTransaction) + { + // The default implementation adds RETURNING 1 to do concurrency check (was the row actually deleted), but in GaussDB we check + // the per-statement row-affected value exposed by GaussDB in the batch; so no need for RETURNING 1. + var name = command.TableName; + var schema = command.Schema; + var conditionOperations = command.ColumnModifications.Where(o => o.IsCondition).ToList(); + + requiresTransaction = false; + + AppendDeleteCommand(commandStringBuilder, name, schema, [], conditionOperations); + + return ResultSetMapping.NoResults; + } + + /// + /// 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 ResultSetMapping AppendStoredProcedureCall( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + out bool requiresTransaction) + { + Check.DebugAssert(command.StoreStoredProcedure is not null, "command.StoreStoredProcedure is not null"); + + var storedProcedure = command.StoreStoredProcedure; + + Check.DebugAssert( + storedProcedure.Parameters.Any() || storedProcedure.ResultColumns.Any(), + "Stored procedure call with neither parameters nor result columns"); + + var resultSetMapping = ResultSetMapping.NoResults; + + commandStringBuilder.Append("CALL "); + + // GaussDB supports neither a return value nor a result set with stored procedures, only output parameters. + Check.DebugAssert(storedProcedure.ReturnValue is null, "storedProcedure.Return is null"); + Check.DebugAssert(!storedProcedure.ResultColumns.Any(), "!storedProcedure.ResultColumns.Any()"); + + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, storedProcedure.Name, storedProcedure.Schema); + + commandStringBuilder.Append('('); + + var first = true; + + // Only positional parameter style supported for now, see https://github.com/dotnet/efcore/issues/28439 + + // Note: the column modifications are already ordered according to the sproc parameter ordering + // (see ModificationCommand.GenerateColumnModifications) + for (var i = 0; i < command.ColumnModifications.Count; i++) + { + var columnModification = command.ColumnModifications[i]; + var parameter = (IStoreStoredProcedureParameter)columnModification.Column!; + + if (first) + { + first = false; + } + else + { + commandStringBuilder.Append(", "); + } + + Check.DebugAssert(columnModification.UseParameter, "Column modification matched a parameter, but UseParameter is false"); + + if (parameter.Direction == ParameterDirection.Output) + { + // Recommended PG practice is to pass NULL for output parameters + commandStringBuilder.Append("NULL"); + } + else + { + SqlGenerationHelper.GenerateParameterNamePlaceholder( + commandStringBuilder, columnModification.UseOriginalValueParameter + ? columnModification.OriginalParameterName! + : columnModification.ParameterName!); + } + + // GaussDB stored procedures cannot return a regular result set, and output parameter values are simply sent back as the + // result set; this is very different from SQL Server, where output parameter values can be sent back in addition to result + // sets. + if (parameter.Direction.HasFlag(ParameterDirection.Output)) + { + // The distinction between having only a rows affected output parameter and having other non-rows affected parameters + // is important later on (i.e. whether we need to propagate or not). + resultSetMapping = parameter == command.RowsAffectedColumn && resultSetMapping == ResultSetMapping.NoResults + ? ResultSetMapping.ResultSetWithRowsAffectedOnly + : ResultSetMapping.LastInResultSet; + } + } + + commandStringBuilder.Append(')'); + + commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); + + requiresTransaction = true; + + return resultSetMapping; + } + + /// + /// 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 AppendObtainNextSequenceValueOperation(StringBuilder commandStringBuilder, string name, string? schema) + { + commandStringBuilder.Append("nextval('"); + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, Check.NotNull(name, nameof(name)), schema); + commandStringBuilder.Append("')"); + } +} diff --git a/src/EFCore.PG/Utilities/SortOrderHelper.cs b/src/EFCore.GaussDB/Utilities/SortOrderHelper.cs similarity index 90% rename from src/EFCore.PG/Utilities/SortOrderHelper.cs rename to src/EFCore.GaussDB/Utilities/SortOrderHelper.cs index 2197ebaf3e..3acc92be54 100644 --- a/src/EFCore.PG/Utilities/SortOrderHelper.cs +++ b/src/EFCore.GaussDB/Utilities/SortOrderHelper.cs @@ -1,6 +1,6 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities; internal static class SortOrderHelper { diff --git a/src/EFCore.PG/Utilities/StringBuilderExtensions.cs b/src/EFCore.GaussDB/Utilities/StringBuilderExtensions.cs similarity index 100% rename from src/EFCore.PG/Utilities/StringBuilderExtensions.cs rename to src/EFCore.GaussDB/Utilities/StringBuilderExtensions.cs diff --git a/src/EFCore.GaussDB/Utilities/Util.cs b/src/EFCore.GaussDB/Utilities/Util.cs new file mode 100644 index 0000000000..84ff9e7438 --- /dev/null +++ b/src/EFCore.GaussDB/Utilities/Util.cs @@ -0,0 +1,24 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Utilities; + +internal static class Statics +{ + internal static readonly bool[][] TrueArrays = + [ + [], + [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 = [ + [], + [false], + [false, false], + [false, false, false] + ]; +} diff --git a/src/EFCore.GaussDB/ValueGeneration/GaussDBSequentialGuidValueGenerator.cs b/src/EFCore.GaussDB/ValueGeneration/GaussDBSequentialGuidValueGenerator.cs new file mode 100644 index 0000000000..349b86aa10 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/GaussDBSequentialGuidValueGenerator.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBSequentialGuidValueGenerator : ValueGenerator +{ + /// + /// Gets a value to be assigned to a property. + /// + /// The change tracking entry of the entity for which the value is being generated. + /// The value to be assigned to a property. + public override Guid Next(EntityEntry entry) + => Guid.CreateVersion7(); + + /// + /// 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; +} diff --git a/src/EFCore.GaussDB/ValueGeneration/GaussDBSequentialStringValueGenerator.cs b/src/EFCore.GaussDB/ValueGeneration/GaussDBSequentialStringValueGenerator.cs new file mode 100644 index 0000000000..6de0987dd3 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/GaussDBSequentialStringValueGenerator.cs @@ -0,0 +1,22 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.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 GaussDBSequentialStringValueGenerator : ValueGenerator +{ + private readonly GaussDBSequentialGuidValueGenerator _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/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceHiLoValueGenerator.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceHiLoValueGenerator.cs new file mode 100644 index 0000000000..f4c28345d8 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceHiLoValueGenerator.cs @@ -0,0 +1,80 @@ +using System.Globalization; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +/// +/// 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 GaussDBSequenceHiLoValueGenerator : HiLoValueGenerator +{ + private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder; + private readonly IUpdateSqlGenerator _sqlGenerator; + private readonly IGaussDBRelationalConnection _connection; + private readonly ISequence _sequence; + private readonly IRelationalCommandDiagnosticsLogger _commandLogger; + + /// + /// 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 GaussDBSequenceHiLoValueGenerator( + IRawSqlCommandBuilder rawSqlCommandBuilder, + IUpdateSqlGenerator sqlGenerator, + GaussDBSequenceValueGeneratorState generatorState, + IGaussDBRelationalConnection connection, + IRelationalCommandDiagnosticsLogger commandLogger) + : base(generatorState) + { + _sequence = generatorState.Sequence; + _rawSqlCommandBuilder = rawSqlCommandBuilder; + _sqlGenerator = sqlGenerator; + _connection = connection; + _commandLogger = commandLogger; + } + + /// + /// 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. + /// + protected override long GetNewLowValue() + => (long)Convert.ChangeType( + _rawSqlCommandBuilder + .Build(_sqlGenerator.GenerateNextSequenceValueOperation(_sequence.Name, _sequence.Schema)) + .ExecuteScalar( + new RelationalCommandParameterObject( + _connection, + parameterValues: null, + readerColumns: null, + context: null, + _commandLogger)), + typeof(long), + CultureInfo.InvariantCulture)!; + + /// + /// 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. + /// + protected override async Task GetNewLowValueAsync(CancellationToken cancellationToken = default) + => (long)Convert.ChangeType( + await _rawSqlCommandBuilder + .Build(_sqlGenerator.GenerateNextSequenceValueOperation(_sequence.Name, _sequence.Schema)) + .ExecuteScalarAsync( + new RelationalCommandParameterObject( + _connection, + parameterValues: null, + readerColumns: null, + context: null, + _commandLogger), + cancellationToken).ConfigureAwait(false), + typeof(long), + CultureInfo.InvariantCulture)!; + + /// + /// 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; +} diff --git a/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceValueGeneratorFactory.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceValueGeneratorFactory.cs new file mode 100644 index 0000000000..e40a6387c7 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceValueGeneratorFactory.cs @@ -0,0 +1,93 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +/// +/// 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 GaussDBSequenceValueGeneratorFactory : IGaussDBSequenceValueGeneratorFactory +{ + private readonly IUpdateSqlGenerator _sqlGenerator; + + /// + /// 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 GaussDBSequenceValueGeneratorFactory( + IUpdateSqlGenerator sqlGenerator) + { + Check.NotNull(sqlGenerator, nameof(sqlGenerator)); + + _sqlGenerator = sqlGenerator; + } + + /// + /// 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? TryCreate( + IProperty property, + Type type, + GaussDBSequenceValueGeneratorState generatorState, + IGaussDBRelationalConnection connection, + IRawSqlCommandBuilder rawSqlCommandBuilder, + IRelationalCommandDiagnosticsLogger commandLogger) + { + if (type == typeof(long)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(int)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(short)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(byte)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(char)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(ulong)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(uint)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(ushort)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + if (type == typeof(sbyte)) + { + return new GaussDBSequenceHiLoValueGenerator( + rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); + } + + return null; + } +} diff --git a/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceValueGeneratorState.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceValueGeneratorState.cs new file mode 100644 index 0000000000..18336e0ba4 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBSequenceValueGeneratorState.cs @@ -0,0 +1,24 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +/// +/// 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 GaussDBSequenceValueGeneratorState : HiLoValueGeneratorState +{ + /// + /// 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 GaussDBSequenceValueGeneratorState(ISequence sequence) + : base(Check.NotNull(sequence, nameof(sequence)).IncrementBy) + { + Sequence = sequence; + } + + /// + /// 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 ISequence Sequence { get; } +} diff --git a/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBValueGeneratorCache.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBValueGeneratorCache.cs new file mode 100644 index 0000000000..d250efee79 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBValueGeneratorCache.cs @@ -0,0 +1,51 @@ +using System.Collections.Concurrent; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +/// +/// 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 GaussDBValueGeneratorCache : ValueGeneratorCache, IGaussDBValueGeneratorCache +{ + private readonly ConcurrentDictionary _sequenceGeneratorCache + = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Parameter object containing dependencies for this service. + public GaussDBValueGeneratorCache(ValueGeneratorCacheDependencies dependencies) + : base(dependencies) + { + } + + /// + /// 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 GaussDBSequenceValueGeneratorState GetOrAddSequenceState( + IProperty property, + IRelationalConnection connection) + { + var sequence = property.FindHiLoSequence(); + + Debug.Assert(sequence is not null); + + return _sequenceGeneratorCache.GetOrAdd( + GetSequenceName(sequence, connection), + _ => new GaussDBSequenceValueGeneratorState(sequence)); + } + + private static string GetSequenceName(ISequence sequence, IRelationalConnection connection) + { + var dbConnection = connection.DbConnection; + + return dbConnection.Database.ToUpperInvariant() + + "::" + + dbConnection.DataSource.ToUpperInvariant() + + "::" + + (sequence.Schema is null ? "" : sequence.Schema + ".") + + sequence.Name; + } +} diff --git a/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBValueGeneratorSelector.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBValueGeneratorSelector.cs new file mode 100644 index 0000000000..b9c9ac2002 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/GaussDBValueGeneratorSelector.cs @@ -0,0 +1,114 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.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 GaussDBValueGeneratorSelector : RelationalValueGeneratorSelector +{ + private readonly IGaussDBSequenceValueGeneratorFactory _sequenceFactory; + private readonly IGaussDBRelationalConnection _connection; + private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder; + private readonly IRelationalCommandDiagnosticsLogger _commandLogger; + + /// + /// 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 GaussDBValueGeneratorSelector( + ValueGeneratorSelectorDependencies dependencies, + IGaussDBSequenceValueGeneratorFactory sequenceFactory, + IGaussDBRelationalConnection connection, + IRawSqlCommandBuilder rawSqlCommandBuilder, + IRelationalCommandDiagnosticsLogger commandLogger) + : base(dependencies) + { + _sequenceFactory = sequenceFactory; + _connection = connection; + _rawSqlCommandBuilder = rawSqlCommandBuilder; + _commandLogger = commandLogger; + } + + /// + /// 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 new virtual IGaussDBValueGeneratorCache Cache + => (IGaussDBValueGeneratorCache)base.Cache; + + /// + /// 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 bool TrySelect(IProperty property, ITypeBase typeBase, out ValueGenerator? valueGenerator) + { + if (property.GetValueGeneratorFactory() != null + || property.GetValueGenerationStrategy() != GaussDBValueGenerationStrategy.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 + /// 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 ValueGenerator? FindForType(IProperty property, ITypeBase typeBase, Type clrType) + => property.ClrType.UnwrapNullableType() switch + { + var t when t == typeof(Guid) && property.ValueGenerated is not ValueGenerated.Never && property.GetDefaultValueSql() is null + => new GaussDBSequentialGuidValueGenerator(), + + var t when t == typeof(string) && property.ValueGenerated is not ValueGenerated.Never && property.GetDefaultValueSql() is null + => new GaussDBSequentialStringValueGenerator(), + + _ => base.FindForType(property, typeBase, clrType) + }; +} diff --git a/src/EFCore.GaussDB/ValueGeneration/Internal/IGaussDBSequenceValueGeneratorFactory.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/IGaussDBSequenceValueGeneratorFactory.cs new file mode 100644 index 0000000000..76fc4ea6aa --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/IGaussDBSequenceValueGeneratorFactory.cs @@ -0,0 +1,22 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +/// +/// 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 interface IGaussDBSequenceValueGeneratorFactory +{ + /// + /// 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? TryCreate( + IProperty property, + Type clrType, + GaussDBSequenceValueGeneratorState generatorState, + IGaussDBRelationalConnection connection, + IRawSqlCommandBuilder rawSqlCommandBuilder, + IRelationalCommandDiagnosticsLogger commandLogger); +} diff --git a/src/EFCore.GaussDB/ValueGeneration/Internal/IGaussDBValueGeneratorCache.cs b/src/EFCore.GaussDB/ValueGeneration/Internal/IGaussDBValueGeneratorCache.cs new file mode 100644 index 0000000000..f37c435a99 --- /dev/null +++ b/src/EFCore.GaussDB/ValueGeneration/Internal/IGaussDBValueGeneratorCache.cs @@ -0,0 +1,16 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +/// +/// 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 interface IGaussDBValueGeneratorCache : IValueGeneratorCache +{ + /// + /// 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. + /// + GaussDBSequenceValueGeneratorState GetOrAddSequenceState( + IProperty property, + IRelationalConnection connection); +} diff --git a/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs b/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs deleted file mode 100644 index 8f33015408..0000000000 --- a/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.Scaffolding.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.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 NpgsqlNetTopologySuiteDesignTimeServices : IDesignTimeServices -{ - /// - /// 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 void ConfigureDesignTimeServices(IServiceCollection serviceCollection) - => serviceCollection - .AddSingleton() - .AddSingleton() - .TryAddSingleton(); -} diff --git a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj deleted file mode 100644 index 46e3bf1098..0000000000 --- a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite - Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite - - Shay Rojansky - NetTopologySuite PostGIS spatial support plugin for PostgreSQL/Npgsql Entity Framework Core provider. - npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;spatial;postgis;nts - README.md - - - - - - - - - - - - - - - - - - - - True - build - - - - diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs deleted file mode 100644 index 2374de24aa..0000000000 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// NetTopologySuite specific extension methods for . -/// -public static class NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions -{ - /// - /// Use NetTopologySuite to access SQL Server spatial data. - /// - /// - /// The options builder so that further configuration can be chained. - /// - public static NpgsqlDbContextOptionsBuilder UseNetTopologySuite( - this NpgsqlDbContextOptionsBuilder optionsBuilder, - CoordinateSequenceFactory? coordinateSequenceFactory = null, - PrecisionModel? precisionModel = null, - Ordinates handleOrdinates = Ordinates.None, - bool geographyAsDefault = false) - { - 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(); - } - - ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); - - return optionsBuilder; - } -} diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs deleted file mode 100644 index 4dabe27a1f..0000000000 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbFunctionsExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides Npgsql-specific spatial extension methods on . -/// -public static class NpgsqlNetTopologySuiteDbFunctionsExtensions -{ - /// - /// Returns a new geometry with its coordinates transformed to a different spatial reference system. - /// Translates to ST_Transform(geometry, srid). - /// - /// - /// See https://postgis.net/docs/ST_Transform.html. - /// - public static TGeometry Transform(this DbFunctions _, TGeometry geometry, int srid) - where TGeometry : Geometry - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Transform))); - - /// - /// Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. - /// Translates to ST_Force2D(geometry) - /// - /// - /// See https://postgis.net/docs/ST_Force2D.html. - /// - public static TGeometry Force2D(this DbFunctions _, TGeometry geometry) - where TGeometry : Geometry - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Force2D))); - - /// - /// Tests whether the distance from the origin geometry to another is less than or equal to a specified value. - /// Translates to ST_DWithin. - /// - /// - /// See https://postgis.net/docs/ST_DWithin.html. - /// - /// The instance. - /// The origin geometry. - /// The geometry to check the distance to. - /// The distance value to compare. - /// Whether to use sphere or spheroid distance measurement. - /// if the geometries are less than distance apart. - public static bool IsWithinDistance(this DbFunctions _, Geometry geometry, Geometry anotherGeometry, double distance, bool useSpheroid) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsWithinDistance))); - - /// - /// Returns the minimum distance between the origin geometry and another geometry g. - /// Translates to ST_Distance. - /// - /// - /// See https://postgis.net/docs/ST_Distance.html. - /// - /// The instance. - /// The origin geometry. - /// The geometry from which to compute the distance. - /// Whether to use sphere or spheroid distance measurement. - /// The distance between the geometries. - public static double Distance(this DbFunctions _, Geometry geometry, Geometry anotherGeometry, bool useSpheroid) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - /// - /// Returns the 2D distance between two geometries. Used in the "ORDER BY" clause, provides index-assisted nearest-neighbor result - /// sets. Translates to <->. - /// - /// - /// See https://postgis.net/docs/ST_Distance.html. - /// - /// The instance. - /// The origin geometry. - /// The geometry from which to compute the distance. - /// The 2D distance between the geometries. - public static double DistanceKnn(this DbFunctions _, Geometry geometry, Geometry anotherGeometry) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceKnn))); -} diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs deleted file mode 100644 index c7a9b27081..0000000000 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite extension methods for . -/// -public static class NpgsqlNetTopologySuiteServiceCollectionExtensions -{ - /// - /// Adds the services required for NetTopologySuite support in the Npgsql provider for Entity Framework. - /// - /// The to add services to. - /// The same service collection so that multiple calls can be chained. - public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite( - this IServiceCollection serviceCollection) - { - Check.NotNull(serviceCollection, nameof(serviceCollection)); - - new EntityFrameworkNpgsqlServicesBuilder(serviceCollection) - .TryAdd() - .TryAdd(p => p.GetRequiredService()) - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAddProviderSpecificServices( - x => x.TryAddSingleton()); - - return serviceCollection; - } -} diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs deleted file mode 100644 index d8e5d7abcd..0000000000 --- a/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteSingletonOptions.cs +++ /dev/null @@ -1,41 +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 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/NpgsqlNetTopologySuiteOptionsExtension.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs deleted file mode 100644 index a217be224c..0000000000 --- a/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -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 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 - /// 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 bool IsGeographyDefault { 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 NpgsqlNetTopologySuiteOptionsExtension() { } - - /// - /// 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 NpgsqlNetTopologySuiteOptionsExtension(NpgsqlNetTopologySuiteOptionsExtension copyFrom) - { - IsGeographyDefault = copyFrom.IsGeographyDefault; - } - - /// - /// 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 NpgsqlNetTopologySuiteOptionsExtension Clone() - => new(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 void ApplyServices(IServiceCollection services) - => services.AddEntityFrameworkNpgsqlNetTopologySuite(); - - /// - /// 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 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 - /// 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 WithGeographyDefault(bool isGeographyDefault = true) - { - var clone = Clone(); - - clone.IsGeographyDefault = isGeographyDefault; - - 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 void Validate(IDbContextOptions options) - { - Check.NotNull(options, nameof(options)); - - var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; - if (internalServiceProvider is not null) - { - using (var scope = internalServiceProvider.CreateScope()) - { - if (scope.ServiceProvider.GetService>() - ?.Any(s => s is NpgsqlNetTopologySuiteTypeMappingSourcePlugin) - != true) - { - throw new InvalidOperationException( - $"{nameof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite)} requires {nameof(NpgsqlNetTopologySuiteServiceCollectionExtensions.AddEntityFrameworkNpgsqlNetTopologySuite)} to be called on the internal service provider used."); - } - } - } - } - - private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) - { - private string? _logFragment; - - private new NpgsqlNetTopologySuiteOptionsExtension Extension - => (NpgsqlNetTopologySuiteOptionsExtension)base.Extension; - - public override bool IsDatabaseProvider - => false; - - public override int GetServiceProviderHashCode() - => Extension.IsGeographyDefault.GetHashCode(); - - public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) - => 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) - { - Check.NotNull(debugInfo, nameof(debugInfo)); - - var prefix = "Npgsql:" + nameof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite); - debugInfo[prefix] = "1"; - debugInfo[$"{prefix}:{nameof(IsGeographyDefault)}"] = Extension.IsGeographyDefault.ToString(); - } - - public override string LogFragment - { - get - { - if (_logFragment is null) - { - var builder = new StringBuilder("using NetTopologySuite"); - if (Extension.IsGeographyDefault) - { - builder.Append(" (geography by default)"); - } - - builder.Append(' '); - - _logFragment = builder.ToString(); - } - - return _logFragment; - } - } - } -} diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs deleted file mode 100644 index d6d6b1805c..0000000000 --- a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlNetTopologySuiteConventionSetPlugin : IConventionSetPlugin -{ - /// - /// 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 ConventionSet ModifyConventions(ConventionSet conventionSet) - { - conventionSet.ModelFinalizingConventions.Add(new NpgsqlNetTopologySuiteExtensionAddingConvention()); - - return conventionSet; - } -} diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs deleted file mode 100644 index 0d44c21aba..0000000000 --- a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlNetTopologySuiteExtensionAddingConvention : IModelFinalizingConvention -{ - /// - /// 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 void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) - => modelBuilder.HasPostgresExtension("postgis"); -} diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteSingletonOptions.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteSingletonOptions.cs deleted file mode 100644 index f34dc4aa19..0000000000 --- a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteSingletonOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; - -/// -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; } - - /// - public virtual void Initialize(IDbContextOptions options) - { - var npgsqlNtsOptions = options.FindExtension() - ?? new NpgsqlNetTopologySuiteOptionsExtension(); - - CoordinateSequenceFactory = npgsqlNtsOptions.CoordinateSequenceFactory; - PrecisionModel = npgsqlNtsOptions.PrecisionModel; - HandleOrdinates = npgsqlNtsOptions.HandleOrdinates; - IsGeographyDefault = npgsqlNtsOptions.IsGeographyDefault; - } - - /// - public virtual void Validate(IDbContextOptions options) { } -} diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs deleted file mode 100644 index 86de979782..0000000000 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin.cs +++ /dev/null @@ -1,155 +0,0 @@ -using NetTopologySuite.Algorithm; -using NetTopologySuite.Geometries.Utilities; -using NetTopologySuite.Operation.Union; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin -{ - /// - /// 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 NpgsqlNetTopologySuiteAggregateMethodCallTranslatorPlugin( - IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) - { - if (sqlExpressionFactory is not NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory) - { - throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); - } - - Translators = - [ - new NpgsqlNetTopologySuiteAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) - ]; - } - - /// - /// 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 IEnumerable Translators { 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 class NpgsqlNetTopologySuiteAggregateMethodTranslator : IAggregateMethodCallTranslator -{ - private static readonly MethodInfo GeometryCombineMethod - = typeof(GeometryCombiner).GetRuntimeMethod(nameof(GeometryCombiner.Combine), [typeof(IEnumerable)])!; - - private static readonly MethodInfo ConvexHullMethod - = typeof(ConvexHull).GetRuntimeMethod(nameof(ConvexHull.Create), [typeof(IEnumerable)])!; - - private static readonly MethodInfo UnionMethod - = typeof(UnaryUnionOp).GetRuntimeMethod(nameof(UnaryUnionOp.Union), [typeof(IEnumerable)])!; - - private static readonly MethodInfo EnvelopeCombineMethod - = typeof(EnvelopeCombiner).GetRuntimeMethod(nameof(EnvelopeCombiner.CombineAsGeometry), [typeof(IEnumerable)])!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - - /// - /// 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 NpgsqlNetTopologySuiteAggregateMethodTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - } - - /// - /// 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? Translate( - MethodInfo method, - EnumerableExpression source, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (source.Selector is not SqlExpression sqlExpression) - { - return null; - } - - if (method == ConvexHullMethod) - { - // PostGIS has no built-in aggregate convex hull, but we can simply apply ST_Collect beforehand as recommended in the docs - // https://postgis.net/docs/ST_ConvexHull.html - return _sqlExpressionFactory.Function( - "ST_ConvexHull", - [ - _sqlExpressionFactory.AggregateFunction( - "ST_Collect", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: [false], - typeof(Geometry), - GetMapping()) - ], - nullable: true, - argumentsPropagateNullability: [true], - typeof(Geometry), - GetMapping()); - } - - if (method == EnvelopeCombineMethod) - { - // ST_Extent returns a PostGIS box2d, which isn't a geometry and has no binary output function. - // Convert it to a geometry first. - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.AggregateFunction( - "ST_Extent", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: [false], - typeof(Geometry), - GetMapping()), - typeof(Geometry), GetMapping()); - } - - if (method == UnionMethod || method == GeometryCombineMethod) - { - return _sqlExpressionFactory.AggregateFunction( - method == UnionMethod ? "ST_Union" : "ST_Collect", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: [false], - typeof(Geometry), - GetMapping()); - } - - return null; - - RelationalTypeMapping? GetMapping() - => _typeMappingSource.FindMapping(typeof(Geometry), sqlExpression.TypeMapping?.StoreType ?? "geometry"); - } -} diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs deleted file mode 100644 index e9fb5ac9f9..0000000000 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs +++ /dev/null @@ -1,187 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlNetTopologySuiteMemberTranslatorPlugin : IMemberTranslatorPlugin -{ - /// - /// 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 NpgsqlNetTopologySuiteMemberTranslatorPlugin( - IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) - { - Translators = [new NpgsqlGeometryMemberTranslator(sqlExpressionFactory, typeMappingSource)]; - } - - /// - /// 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 IEnumerable Translators { 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 class NpgsqlGeometryMemberTranslator : IMemberTranslator -{ - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly CaseWhenClause[] _ogcGeometryTypeWhenThenList; - - 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 - /// 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 NpgsqlGeometryMemberTranslator( - ISqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - - _ogcGeometryTypeWhenThenList = - [ - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_CircularString"), _sqlExpressionFactory.Constant(OgcGeometryType.CircularString)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_CompoundCurve"), _sqlExpressionFactory.Constant(OgcGeometryType.CompoundCurve)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_CurvePolygon"), _sqlExpressionFactory.Constant(OgcGeometryType.CurvePolygon)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_GeometryCollection"), - _sqlExpressionFactory.Constant(OgcGeometryType.GeometryCollection)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_LineString"), _sqlExpressionFactory.Constant(OgcGeometryType.LineString)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_MultiCurve"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiCurve)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_MultiLineString"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiLineString)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_MultiPoint"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiPoint)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_MultiPolygon"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiPolygon)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_MultiSurface"), _sqlExpressionFactory.Constant(OgcGeometryType.MultiSurface)), - new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Point"), _sqlExpressionFactory.Constant(OgcGeometryType.Point)), - new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Polygon"), _sqlExpressionFactory.Constant(OgcGeometryType.Polygon)), - new CaseWhenClause( - _sqlExpressionFactory.Constant("ST_PolyhedralSurface"), - _sqlExpressionFactory.Constant(OgcGeometryType.PolyhedralSurface)), - new CaseWhenClause(_sqlExpressionFactory.Constant("ST_Tin"), _sqlExpressionFactory.Constant(OgcGeometryType.TIN)) - ]; - } - - /// - /// 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? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - var declaringType = member.DeclaringType; - - if (instance is null || !typeof(Geometry).IsAssignableFrom(declaringType)) - { - return null; - } - - var typeMapping = instance.TypeMapping; - Debug.Assert(typeMapping is not null, "Instance must have typeMapping assigned."); - var storeType = instance.TypeMapping!.StoreType; - - if (typeof(Point).IsAssignableFrom(declaringType)) - { - var function = member.Name switch - { - nameof(Point.X) => "ST_X", - nameof(Point.Y) => "ST_Y", - nameof(Point.Z) => "ST_Z", - nameof(Point.M) => "ST_M", - _ => null - }; - - if (function is not null) - { - return Function(function, [instance], typeof(double)); - } - } - - if (typeof(LineString).IsAssignableFrom(declaringType)) - { - if (member.Name == "Count") - { - return Function("ST_NumPoints", [instance], typeof(int)); - } - } - - return member.Name switch - { - 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", [instance], typeof(string)), - _ogcGeometryTypeWhenThenList, - elseResult: null), - - _ => null - }; - - SqlExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) - => _sqlExpressionFactory.Function( - name, arguments, - nullable: true, argumentsPropagateNullability: TrueArrays[arguments.Length], - returnType, typeMapping); - - RelationalTypeMapping ResultGeometryMapping() - { - Debug.Assert(typeof(Geometry).IsAssignableFrom(returnType)); - return _typeMappingSource.FindMapping(returnType, storeType)!; - } - } -} diff --git a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs deleted file mode 100644 index b6cd534eb6..0000000000 --- a/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs +++ /dev/null @@ -1,248 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlNetTopologySuiteMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin -{ - /// - /// 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 NpgsqlNetTopologySuiteMethodCallTranslatorPlugin( - IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) - { - if (sqlExpressionFactory is not NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory) - { - throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); - } - - Translators = [new NpgsqlGeometryMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource)]; - } - - /// - /// 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 IEnumerable Translators { 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 class NpgsqlGeometryMethodTranslator : IMethodCallTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - - private static readonly bool[][] TrueArrays = - [ - [], [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 - /// 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 NpgsqlGeometryMethodTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - => method.DeclaringType switch - { - var t when typeof(Geometry).IsAssignableFrom(t) && instance is not null - => TranslateGeometryMethod(instance, method, arguments), - - var t when t == typeof(NpgsqlNetTopologySuiteDbFunctionsExtensions) - => TranslateDbFunction(method, arguments), - - // This handles the collection indexer (geom_collection[x] -> ST_GeometryN(geom_collection, x + 1)) - // This is needed as a special case because EF transforms the indexer into a call to Enumerable.ElementAt - var t when t == typeof(Enumerable) - && method.Name is nameof(Enumerable.ElementAt) - && method.ReturnType == typeof(Geometry) - && arguments is [var collection, var index] - && _typeMappingSource.FindMapping(typeof(Geometry), collection.TypeMapping!.StoreType) is RelationalTypeMapping geometryTypeMapping - => _sqlExpressionFactory.Function( - "ST_GeometryN", - [collection, OneBased(index)], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - geometryTypeMapping), - - _ => null - }; - - private SqlExpression? TranslateDbFunction( - MethodInfo method, - IReadOnlyList arguments) - => method.Name switch - { - nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Transform) => _sqlExpressionFactory.Function( - "ST_Transform", - [arguments[1], arguments[2]], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - arguments[1].TypeMapping), - - nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Force2D) => _sqlExpressionFactory.Function( - "ST_Force2D", - [arguments[1]], - nullable: true, - TrueArrays[1], - method.ReturnType, - arguments[1].TypeMapping), - - nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.DistanceKnn) => _sqlExpressionFactory.MakePostgresBinary( - PgExpressionType.Distance, - arguments[1], - arguments[2]), - - nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.Distance) => - TranslateGeometryMethod(arguments[1], method, [arguments[2], arguments[3]]), - nameof(NpgsqlNetTopologySuiteDbFunctionsExtensions.IsWithinDistance) => - TranslateGeometryMethod(arguments[1], method, [arguments[2], arguments[3], arguments[4]]), - - _ => null - }; - - private SqlExpression? TranslateGeometryMethod( - SqlExpression instance, - MethodInfo method, - IReadOnlyList arguments) - { - var typeMapping = ExpressionExtensions.InferTypeMapping( - arguments.Prepend(instance).Where(e => typeof(Geometry).IsAssignableFrom(e.Type)).ToArray()); - - Debug.Assert(typeMapping is not null, "At least one argument must have typeMapping."); - var storeType = typeMapping.StoreType; - - instance = _sqlExpressionFactory.ApplyTypeMapping(instance, _typeMappingSource.FindMapping(instance.Type, storeType)); - - var typeMappedArguments = new List(); - foreach (var argument in arguments) - { - typeMappedArguments.Add( - _sqlExpressionFactory.ApplyTypeMapping( - argument, - typeof(Geometry).IsAssignableFrom(argument.Type) - ? _typeMappingSource.FindMapping(argument.Type, storeType) - : _typeMappingSource.FindMapping(argument.Type))); - } - - arguments = typeMappedArguments; - - return method.Name switch - { - nameof(Geometry.AsBinary) - => Function("ST_AsBinary", [instance], typeof(byte[])), - nameof(Geometry.AsText) - => 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", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.ConvexHull) - => Function("ST_ConvexHull", [instance], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.CoveredBy) - => Function("ST_CoveredBy", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Covers) - => Function("ST_Covers", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Crosses) - => Function("ST_Crosses", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Disjoint) - => Function("ST_Disjoint", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Difference) - => 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", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.EqualsTopologically) - => Function("ST_Equals", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.GetGeometryN) - => Function("ST_GeometryN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), - nameof(Polygon.GetInteriorRingN) - => Function("ST_InteriorRingN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), - nameof(LineString.GetPointN) - => Function("ST_PointN", [instance, OneBased(arguments[0])], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.Intersection) - => Function("ST_Intersection", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.Intersects) - => 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", [instance], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.Overlaps) - => Function("ST_Overlaps", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Relate) - => Function("ST_Relate", [instance, arguments[0], arguments[1]], typeof(bool)), - nameof(Geometry.Reverse) - => Function("ST_Reverse", [instance], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.SymmetricDifference) - => Function("ST_SymDifference", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.ToBinary) - => Function("ST_AsBinary", [instance], typeof(byte[])), - nameof(Geometry.ToText) - => Function("ST_AsText", [instance], typeof(string)), - nameof(Geometry.Touches) - => Function("ST_Touches", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Within) - => Function("ST_Within", [instance, arguments[0]], typeof(bool)), - nameof(Geometry.Union) when arguments.Count == 0 - => Function("ST_UnaryUnion", [instance], typeof(Geometry), ResultGeometryMapping()), - nameof(Geometry.Union) when arguments.Count == 1 - => Function("ST_Union", [instance, arguments[0]], typeof(Geometry), ResultGeometryMapping()), - - _ => null - }; - - SqlExpression Function(string name, SqlExpression[] arguments, Type returnType, RelationalTypeMapping? typeMapping = null) - => _sqlExpressionFactory.Function( - name, arguments, - nullable: true, argumentsPropagateNullability: TrueArrays[arguments.Length], - returnType, typeMapping); - - RelationalTypeMapping ResultGeometryMapping() - { - Debug.Assert(typeof(Geometry).IsAssignableFrom(method.ReturnType)); - return _typeMappingSource.FindMapping(method.ReturnType, storeType)!; - } - } - - // NetTopologySuite uses 0-based indexing, but PostGIS uses 1-based - private SqlExpression OneBased(SqlExpression arg) - => arg is SqlConstantExpression constant - ? _sqlExpressionFactory.Constant((int)constant.Value! + 1, constant.TypeMapping) - : _sqlExpressionFactory.Add(arg, _sqlExpressionFactory.Constant(1)); -} diff --git a/src/EFCore.PG.NTS/README.md b/src/EFCore.PG.NTS/README.md deleted file mode 100644 index ee89907bce..0000000000 --- a/src/EFCore.PG.NTS/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Npgsql Entity Framework Core provider for PostgreSQL - -Npgsql.EntityFrameworkCore.PostgreSQL is the open source EF Core provider for PostgreSQL. It allows you to interact with PostgreSQL via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. - -This package is a plugin which allows you to interact with spatial data provided by the PostgreSQL [PostGIS extension](https://postgis.net); PostGIS is a mature, standard extension considered to provide top-of-the-line database spatial features. On the .NET side, the plugin adds support for the types from the [NetTopologySuite library](https://github.com/NetTopologySuite/NetTopologySuite), allowing you to read and write them directly to PostgreSQL. - -To use the plugin, simply add `UseNetTopologySuite` as below and use NetTopologySuite types in your entity properties: - -```csharp -await using var ctx = new BlogContext(); -await ctx.Database.EnsureDeletedAsync(); -await ctx.Database.EnsureCreatedAsync(); - -// Insert a Blog -ctx.Cities.Add(new() -{ - Name = "FooCity", - Center = new Point(10, 10) -}); -await ctx.SaveChangesAsync(); - -// Query all cities with the given center point -var newBlogs = await ctx.Cities.Where(b => b.Center == new Point(10, 10)).ToListAsync(); - -public class BlogContext : DbContext -{ - public DbSet Cities { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql( - @"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase", - o => o.UseNetTopologySuite()); -} - -public class City -{ - public int Id { get; set; } - public string Name { get; set; } - public Point Center { get; set; } -} -``` - -The plugin also supports translating many NetTopologySuite methods and properties into corresponding PostGIS operations. For more information, see the [NetTopologySuite plugin documentation page](https://www.npgsql.org/efcore/mapping/nts.html). diff --git a/src/EFCore.PG.NTS/Scaffolding/Internal/NpgsqlNetTopologySuiteCodeGeneratorPlugin.cs b/src/EFCore.PG.NTS/Scaffolding/Internal/NpgsqlNetTopologySuiteCodeGeneratorPlugin.cs deleted file mode 100644 index 5b3f18a1ab..0000000000 --- a/src/EFCore.PG.NTS/Scaffolding/Internal/NpgsqlNetTopologySuiteCodeGeneratorPlugin.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.Scaffolding.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 NpgsqlNetTopologySuiteCodeGeneratorPlugin : ProviderCodeGeneratorPlugin -{ - private static readonly MethodInfo _useNetTopologySuiteMethodInfo - = typeof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite), - typeof(NpgsqlDbContextOptionsBuilder), - typeof(CoordinateSequenceFactory), - typeof(PrecisionModel), - typeof(Ordinates), - typeof(bool)); - - /// - /// 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 MethodCallCodeFragment GenerateProviderOptions() - => new(_useNetTopologySuiteMethodInfo); -} diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs deleted file mode 100644 index 8a9fa940dd..0000000000 --- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Data.Common; -using System.Text; -using JetBrains.Annotations; -using NetTopologySuite.IO; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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. -/// -[UsedImplicitly] -public class NpgsqlGeometryTypeMapping : RelationalGeometryTypeMapping, INpgsqlTypeMapping -{ - private readonly bool _isGeography; - - /// - public virtual NpgsqlDbType NpgsqlDbType - => _isGeography ? NpgsqlDbType.Geography : NpgsqlDbType.Geometry; - - /// - /// 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 NpgsqlGeometryTypeMapping(string storeType, bool isGeography) - : base(converter: null, storeType, NpgsqlJsonGeometryWktReaderWriter.Instance) - { - _isGeography = isGeography; - } - - /// - /// 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 NpgsqlGeometryTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, converter: 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 override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlGeometryTypeMapping(parameters); - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - { - base.ConfigureParameter(parameter); - - ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType; - } - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var geometry = (Geometry)value; - var builder = new StringBuilder(); - - builder - .Append(_isGeography ? "GEOGRAPHY" : "GEOMETRY") - .Append(" '"); - - if (geometry.SRID > 0) - { - builder - .Append("SRID=") - .Append(geometry.SRID) - .Append(';'); - } - - builder - .Append(geometry.AsText()) - .Append('\''); - - return builder.ToString(); - } - - /// - /// 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 AsText(object value) - => ((Geometry)value).AsText(); - - /// - /// 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 int GetSrid(object value) - => ((Geometry)value).SRID; - - /// - /// 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 Type WktReaderType - => typeof(WKTReader); -} diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs deleted file mode 100644 index 4685aa0cec..0000000000 --- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlJsonGeometryWktReaderWriter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; -using NetTopologySuite.IO; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -/// -/// Reads and writes JSON using the well-known-text format for values. -/// -public sealed class NpgsqlJsonGeometryWktReaderWriter : JsonValueReaderWriter -{ - private static readonly PropertyInfo InstanceProperty = typeof(NpgsqlJsonGeometryWktReaderWriter).GetProperty(nameof(Instance))!; - - private static readonly WKTReader WktReader = new(); - - /// - /// The singleton instance of this stateless reader/writer. - /// - public static NpgsqlJsonGeometryWktReaderWriter Instance { get; } = new(); - - private NpgsqlJsonGeometryWktReaderWriter() - { - } - - /// - public override Geometry FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) - => WktReader.Read(manager.CurrentReader.GetString()); - - /// - public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) - { - 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 deleted file mode 100644 index 2dcbd5a777..0000000000 --- a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlNetTopologySuiteTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin -{ - // Note: we reference the options rather than copying IsGeographyDefault out, because that field is initialized - // rather late by SingletonOptionsInitializer - private readonly INpgsqlNetTopologySuiteSingletonOptions _options; - - private static bool TryGetClrType(string subtypeName, [NotNullWhen(true)] out Type? clrType) - { - clrType = subtypeName switch - { - "POINT" => typeof(Point), - "LINESTRING" => typeof(LineString), - "POLYGON" => typeof(Polygon), - "MULTIPOINT" => typeof(MultiPoint), - "MULTILINESTRING" => typeof(MultiLineString), - "MULTIPOLYGON" => typeof(MultiPolygon), - "GEOMETRYCOLLECTION" => typeof(GeometryCollection), - "GEOMETRY" => typeof(Geometry), - _ => null - }; - - return clrType is not 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 NpgsqlNetTopologySuiteTypeMappingSourcePlugin(INpgsqlNetTopologySuiteSingletonOptions options) - { - _options = Check.NotNull(options, nameof(options)); - } - - /// - /// 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 RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) - { - // TODO: Array - var clrType = mappingInfo.ClrType; - var storeTypeName = mappingInfo.StoreTypeName; - var isGeography = _options.IsGeographyDefault; - - if (clrType is not null) - { - if (!clrType.IsAssignableTo(typeof(Geometry))) - { - return null; - } - - // TODO: if store type is null, consider setting it based on the CLR type, i.e. create GEOMETRY(Point) instead of Geometry when - // the CLR property is NTS Point. - } - - if (storeTypeName is not null) - { - if (!TryParseStoreTypeName(storeTypeName, out _, out isGeography, out var parsedSubtype, out _, out _)) - { - return null; - } - - clrType ??= parsedSubtype; - } - - storeTypeName ??= isGeography ? "geography" : "geometry"; - - Check.DebugAssert(clrType is not null, "clrType is not null"); - - var typeMapping = (RelationalTypeMapping)Activator.CreateInstance( - typeof(NpgsqlGeometryTypeMapping<>).MakeGenericType(clrType), storeTypeName, isGeography)!; - - // TODO: for geometry collection support (and why the following is commented out), see #2850. - - // // TODO: Also restrict the element type mapping based on the user-specified store type? - // var elementType = clrType == typeof(MultiPoint) - // ? typeof(Point) - // : clrType == typeof(MultiLineString) - // ? typeof(LineString) - // : clrType == typeof(MultiPolygon) - // ? typeof(Polygon) - // : clrType == typeof(GeometryCollection) - // ? typeof(Geometry) - // : null; - // - // if (elementType is not null) - // { - // var elementTypeMapping = FindMapping(new() { ClrType = elementType })!; - // - // typeMapping = typeMapping.Clone(elementMapping: elementTypeMapping); - // } - - return typeMapping; - } - - /// - /// Given a PostGIS store type name (e.g. GEOMETRY, GEOGRAPHY(Point, 4326), GEOMETRY(LineStringM, 4326)), - /// attempts to parse it and return its components. - /// - public static bool TryParseStoreTypeName( - string storeTypeName, - out string subtypeName, - out bool isGeography, - out Type? clrType, - out int srid, - out Ordinates ordinates) - { - storeTypeName = storeTypeName.Trim(); - subtypeName = storeTypeName; - isGeography = false; - clrType = typeof(Geometry); - srid = -1; - ordinates = Ordinates.AllOrdinates; - - var openParen = storeTypeName.IndexOf("(", StringComparison.Ordinal); - - var baseType = openParen > 0 ? storeTypeName.Substring(0, openParen).Trim() : storeTypeName; - - if (baseType.Equals("GEOMETRY", StringComparison.OrdinalIgnoreCase)) - { - isGeography = false; - } - else if (baseType.Equals("GEOGRAPHY", StringComparison.OrdinalIgnoreCase)) - { - isGeography = true; - } - else - { - return false; - } - - if (openParen == -1) - { - return true; - } - - var closeParen = storeTypeName.IndexOf(")", openParen + 1, StringComparison.Ordinal); - if (closeParen != storeTypeName.Length - 1) - { - return false; - } - - var comma = storeTypeName.IndexOf(",", openParen + 1, StringComparison.Ordinal); - if (comma == -1) - { - subtypeName = storeTypeName.Substring(openParen + 1, closeParen - openParen - 1).Trim(); - } - else - { - subtypeName = storeTypeName.Substring(openParen + 1, comma - openParen - 1).Trim(); - - if (!int.TryParse(storeTypeName.Substring(comma + 1, closeParen - comma - 1).Trim(), out srid)) - { - return false; - } - } - - subtypeName = subtypeName.ToUpper(CultureInfo.InvariantCulture); - - // We have geometry(subtype, srid), parse the subtype (POINT, POINTZ, POINTM, POINTZM...) - - if (TryGetClrType(subtypeName, out clrType)) - { - return true; - } - - if (subtypeName.EndsWith("ZM", StringComparison.Ordinal) && TryGetClrType(subtypeName[..^2], out clrType)) - { - ordinates = Ordinates.XYZM; - return true; - } - - if (subtypeName.EndsWith("M", StringComparison.Ordinal) && TryGetClrType(subtypeName[..^1], out clrType)) - { - ordinates = Ordinates.XYM; - return true; - } - - if (subtypeName.EndsWith("Z", StringComparison.Ordinal) && TryGetClrType(subtypeName[..^1], out clrType)) - { - ordinates = Ordinates.XYZ; - return true; - } - - return false; - } -} diff --git a/src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets b/src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets deleted file mode 100644 index e5f3b396c9..0000000000 --- a/src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets +++ /dev/null @@ -1,46 +0,0 @@ - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - $(IntermediateOutputPath)EFCoreNpgsqlNetTopologySuite$(DefaultLanguageSourceExtension) - - - - - - - CompileBefore - - - - - CompileAfter - - - - - - - Compile - - - - - - - <_Parameter1>Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal.NpgsqlNetTopologySuiteDesignTimeServices, Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite - <_Parameter2>Npgsql.EntityFrameworkCore.PostgreSQL - - - - - - - - diff --git a/src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs b/src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs deleted file mode 100644 index b1e813fe07..0000000000 --- a/src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs +++ /dev/null @@ -1,27 +0,0 @@ -using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Scaffolding.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.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. -/// -[UsedImplicitly] -public class NpgsqlNodaTimeDesignTimeServices : IDesignTimeServices -{ - /// - /// 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 void ConfigureDesignTimeServices(IServiceCollection serviceCollection) - => serviceCollection - .AddSingleton() - .AddSingleton(); -} diff --git a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj deleted file mode 100644 index 5d4c0d4a41..0000000000 --- a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime - Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime - - Shay Rojansky - NodaTime support plugin for PostgreSQL/Npgsql Entity Framework Core provider. - npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;nodatime;date;time - README.md - - - - - - - - - - - - - - - - - - - True - build - - - - diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs deleted file mode 100644 index 3af53dc7ce..0000000000 --- a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// NodaTime specific extension methods for . -/// -public static class NpgsqlNodaTimeDbContextOptionsBuilderExtensions -{ - /// - /// Configure NodaTime type mappings for Entity Framework. - /// - /// The options builder so that further configuration can be chained. - public static NpgsqlDbContextOptionsBuilder UseNodaTime( - this NpgsqlDbContextOptionsBuilder optionsBuilder) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - - var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; - - var extension = coreOptionsBuilder.Options.FindExtension() - ?? new NpgsqlNodaTimeOptionsExtension(); - - ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); - - return optionsBuilder; - } -} diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbFunctionsExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbFunctionsExtensions.cs deleted file mode 100644 index 88d0c941fa..0000000000 --- a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbFunctionsExtensions.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods supporting NodaTime function translation for PostgreSQL. -/// -public static class NpgsqlNodaTimeDbFunctionsExtensions -{ - /// - /// Computes the sum of the non-null input intervals. Corresponds to the PostgreSQL sum aggregate function. - /// - /// The instance. - /// The input values to be summed. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static Period? Sum(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Sum))); - - /// - /// Computes the sum of the non-null input intervals. Corresponds to the PostgreSQL sum aggregate function. - /// - /// The instance. - /// The input values to be summed. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static Duration? Sum(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Sum))); - - /// - /// Computes the average (arithmetic mean) of the non-null input intervals. Corresponds to the PostgreSQL avg aggregate function. - /// - /// The instance. - /// The input values to be computed into an average. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static Period? Average(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); - - /// - /// Computes the average (arithmetic mean) of the non-null input intervals. Corresponds to the PostgreSQL avg aggregate function. - /// - /// The instance. - /// The input values to be computed into an average. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static Duration? Average(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); - - /// - /// Returns the distance between two instants as a , particularly suitable for sorting where the appropriate index - /// is defined. - /// - /// - /// This requires the btree_gist built-in PostgreSQL extension, see - /// . - /// - public static int Distance(this DbFunctions _, Instant a, Instant b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - /// - /// Returns the distance between two zoned timestamps as a , particularly suitable for sorting where the - /// appropriate index is defined. - /// - /// - /// This requires the btree_gist built-in PostgreSQL extension, see - /// . - /// - public static int Distance(this DbFunctions _, ZonedDateTime a, ZonedDateTime b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - /// - /// Returns the distance between two local timestamps as a , particularly suitable for sorting where the - /// appropriate index is defined. - /// - /// - /// This requires the btree_gist built-in PostgreSQL extension, see - /// . - /// - public static int Distance(this DbFunctions _, LocalDateTime a, LocalDateTime b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - /// - /// Returns the distance between two dates as a number of days, particularly suitable for sorting where the appropriate index is - /// defined. - /// - /// - /// This requires the btree_gist built-in PostgreSQL extension, see - /// . - /// - public static int Distance(this DbFunctions _, LocalDate a, LocalDate b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - #region Aggregate functions - - /// - /// Computes the union of the non-null input intervals. Corresponds to the PostgreSQL range_agg aggregate function. - /// - /// The instance. - /// The intervals to be aggregated via union into a multirange. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static Interval[] RangeAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeAgg))); - - /// - /// Computes the union of the non-null input date intervals. Corresponds to the PostgreSQL range_agg aggregate function. - /// - /// The instance. - /// The date intervals to be aggregated via union into a multirange. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static DateInterval[] RangeAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeAgg))); - - /// - /// Computes the intersection of the non-null input intervals. Corresponds to the PostgreSQL range_intersect_agg aggregate function. - /// - /// The instance. - /// The intervals on which to perform the intersection operation. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static Interval RangeIntersectAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); - - /// - /// Computes the intersection of the non-null input date intervals. Corresponds to the PostgreSQL range_intersect_agg aggregate - /// function. - /// - /// The instance. - /// The date intervals on which to perform the intersection operation. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static DateInterval RangeIntersectAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); - - /// - /// Computes the intersection of the non-null input interval multiranges. - /// Corresponds to the PostgreSQL range_intersect_agg aggregate function. - /// - /// The instance. - /// The intervals on which to perform the intersection operation. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static Interval[] RangeIntersectAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); - - /// - /// Computes the intersection of the non-null input date interval multiranges. - /// Corresponds to the PostgreSQL range_intersect_agg aggregate function. - /// - /// The instance. - /// The date intervals on which to perform the intersection operation. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of - /// an EF Core LINQ query. - /// - public static DateInterval[] RangeIntersectAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); - - #endregion Aggregate functions -} diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs deleted file mode 100644 index 3776737368..0000000000 --- a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime extension methods for . -/// -public static class NpgsqlNodaTimeServiceCollectionExtensions -{ - /// - /// Adds the services required for NodaTime support in the Npgsql provider for Entity Framework. - /// - /// The to add services to. - /// The same service collection so that multiple calls can be chained. - public static IServiceCollection AddEntityFrameworkNpgsqlNodaTime( - this IServiceCollection serviceCollection) - { - Check.NotNull(serviceCollection, nameof(serviceCollection)); - - new EntityFrameworkNpgsqlServicesBuilder(serviceCollection) - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd(); - - return serviceCollection; - } -} diff --git a/src/EFCore.PG.NodaTime/Extensions/TypeExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/TypeExtensions.cs deleted file mode 100644 index 56824fd623..0000000000 --- a/src/EFCore.PG.NodaTime/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -internal static class TypeExtensions -{ - internal static bool IsGenericList(this Type type) - => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); - - internal static bool IsArrayOrGenericList(this Type type) - => type.IsArray || type.IsGenericList(); - - internal static bool TryGetElementType(this Type type, out Type? elementType) - { - elementType = type.IsArray - ? type.GetElementType() - : type.IsGenericList() - ? type.GetGenericArguments()[0] - : null; - return elementType is not null; - } -} diff --git a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs deleted file mode 100644 index 0c070cf114..0000000000 --- a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -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 NpgsqlNodaTimeOptionsExtension : 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 void ApplyServices(IServiceCollection services) - => services.AddEntityFrameworkNpgsqlNodaTime(); - - /// - /// 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 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 void Validate(IDbContextOptions options) - { - var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; - if (internalServiceProvider is not null) - { - using (var scope = internalServiceProvider.CreateScope()) - { - if (scope.ServiceProvider.GetService>() - ?.Any(s => s is NpgsqlNodaTimeTypeMappingSourcePlugin) - != true) - { - throw new InvalidOperationException( - $"{nameof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime)} requires {nameof(NpgsqlNodaTimeServiceCollectionExtensions.AddEntityFrameworkNpgsqlNodaTime)} to be called on the internal service provider used."); - } - } - } - } - - private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) - { - private new NpgsqlNodaTimeOptionsExtension Extension - => (NpgsqlNodaTimeOptionsExtension)base.Extension; - - public override bool IsDatabaseProvider - => false; - - public override int GetServiceProviderHashCode() - => 0; - - public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) - => true; - - public override void PopulateDebugInfo(IDictionary debugInfo) - => debugInfo["Npgsql:" + nameof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime)] = "1"; - - public override string LogFragment - => "using NodaTime "; - } -} diff --git a/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs b/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs deleted file mode 100644 index 75fa2f8357..0000000000 --- a/src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: - InternalsVisibleTo( - "Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests, PublicKey=" - + "0024000004800000940000000602000000240000525341310004000001000100" - + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" - + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" - + "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" - + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] diff --git a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs deleted file mode 100644 index 32139bda95..0000000000 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.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 NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin : IAggregateMethodCallTranslatorPlugin -{ - /// - /// 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 NpgsqlNodaTimeAggregateMethodCallTranslatorPlugin( - ISqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - if (sqlExpressionFactory is not NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory) - { - throw new ArgumentException($"Must be an {nameof(NpgsqlSqlExpressionFactory)}", nameof(sqlExpressionFactory)); - } - - Translators = - [ - new NpgsqlNodaTimeAggregateMethodTranslator(npgsqlSqlExpressionFactory, typeMappingSource) - ]; - } - - /// - /// 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 IEnumerable Translators { 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 class NpgsqlNodaTimeAggregateMethodTranslator : IAggregateMethodCallTranslator -{ - private static readonly bool[][] FalseArrays = [[], [false]]; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - - /// - /// 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 NpgsqlNodaTimeAggregateMethodTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - } - - /// - /// 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? Translate( - MethodInfo method, - EnumerableExpression source, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (source.Selector is not SqlExpression sqlExpression || method.DeclaringType != typeof(NpgsqlNodaTimeDbFunctionsExtensions)) - { - return null; - } - - return method.Name switch - { - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Sum) => _sqlExpressionFactory.AggregateFunction( - "sum", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], - returnType: sqlExpression.Type, sqlExpression.TypeMapping), - - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Average) => _sqlExpressionFactory.AggregateFunction( - "avg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], - returnType: sqlExpression.Type, sqlExpression.TypeMapping), - - nameof(NpgsqlNodaTimeDbFunctionsExtensions.RangeAgg) => _sqlExpressionFactory.AggregateFunction( - "range_agg", [sqlExpression], source, nullable: true, argumentsPropagateNullability: FalseArrays[1], - returnType: method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType)), - - nameof(NpgsqlNodaTimeDbFunctionsExtensions.RangeIntersectAgg) => _sqlExpressionFactory.AggregateFunction( - "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 deleted file mode 100644 index 6e837baf6d..0000000000 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeEvaluatableExpressionFilterPlugin.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.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 NpgsqlNodaTimeEvaluatableExpressionFilterPlugin : IEvaluatableExpressionFilterPlugin -{ - private static readonly MethodInfo GetCurrentInstantMethod = - typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), [])!; - - private static readonly MemberInfo SystemClockInstanceMember = - typeof(SystemClock).GetMember(nameof(SystemClock.Instance)).FirstOrDefault()!; - - /// - /// 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 bool IsEvaluatableExpression(Expression expression) - { - switch (expression) - { - case MethodCallExpression methodCallExpression when methodCallExpression.Method == GetCurrentInstantMethod: - return false; - - case MemberExpression memberExpression: - if (memberExpression.Member == SystemClockInstanceMember) - { - return false; - } - - // We support translating certain NodaTime patterns which accept a time zone as a parameter, - // e.g. Instant.InZone(timezone), as long as the timezone is expressed as an access on DateTimeZoneProviders.Tzdb. - // Prevent this from being evaluated locally and so parameterized, so we can access the member access on - // DateTimeZoneProviders and extract the constant (see NpgsqlNodaTimeMethodCallTranslatorPlugin) - if (memberExpression.Member.DeclaringType == typeof(DateTimeZoneProviders)) - { - return false; - } - - break; - } - - return true; - } -} diff --git a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs deleted file mode 100644 index cda2806302..0000000000 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs +++ /dev/null @@ -1,395 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; - -/// -/// Provides translation services for members. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-datetime.html -/// -public class NpgsqlNodaTimeMemberTranslatorPlugin : IMemberTranslatorPlugin -{ - /// - /// 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 NpgsqlNodaTimeMemberTranslatorPlugin( - IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) - { - Translators = - [ - new NpgsqlNodaTimeMemberTranslator(typeMappingSource, (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 virtual IEnumerable Translators { 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 class NpgsqlNodaTimeMemberTranslator : IMemberTranslator -{ - private static readonly MemberInfo SystemClock_Instance = - typeof(SystemClock).GetRuntimeProperty(nameof(SystemClock.Instance))!; - - private static readonly MemberInfo ZonedDateTime_LocalDateTime = - typeof(ZonedDateTime).GetRuntimeProperty(nameof(ZonedDateTime.LocalDateTime))!; - - private static readonly MemberInfo Interval_Start = - typeof(Interval).GetRuntimeProperty(nameof(Interval.Start))!; - - private static readonly MemberInfo Interval_End = - typeof(Interval).GetRuntimeProperty(nameof(Interval.End))!; - - private static readonly MemberInfo Interval_HasStart = - typeof(Interval).GetRuntimeProperty(nameof(Interval.HasStart))!; - - private static readonly MemberInfo Interval_HasEnd = - typeof(Interval).GetRuntimeProperty(nameof(Interval.HasEnd))!; - - private static readonly MemberInfo Interval_Duration = - typeof(Interval).GetRuntimeProperty(nameof(Interval.Duration))!; - - private static readonly MemberInfo DateInterval_Start = - typeof(DateInterval).GetRuntimeProperty(nameof(DateInterval.Start))!; - - private static readonly MemberInfo DateInterval_End = - typeof(DateInterval).GetRuntimeProperty(nameof(DateInterval.End))!; - - private static readonly MemberInfo DateInterval_Length = - typeof(DateInterval).GetRuntimeProperty(nameof(DateInterval.Length))!; - - private static readonly MemberInfo DateTimeZoneProviders_TzDb = - typeof(DateTimeZoneProviders).GetRuntimeProperty(nameof(DateTimeZoneProviders.Tzdb))!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly RelationalTypeMapping _dateTypeMapping; - private readonly RelationalTypeMapping _periodTypeMapping; - private readonly RelationalTypeMapping _localDateTimeTypeMapping; - - /// - /// 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 NpgsqlNodaTimeMemberTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - _dateTypeMapping = typeMappingSource.FindMapping(typeof(LocalDate))!; - _periodTypeMapping = typeMappingSource.FindMapping(typeof(Period))!; - _localDateTimeTypeMapping = typeMappingSource.FindMapping(typeof(LocalDateTime))!; - } - - private static readonly bool[][] TrueArrays = [[], [true], [true, true]]; - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - // This is necessary to allow translation of methods on SystemClock.Instance - if (member == SystemClock_Instance) - { - return _sqlExpressionFactory.Constant(SystemClock.Instance); - } - - if (member == DateTimeZoneProviders_TzDb) - { - return PendingDateTimeZoneProviderExpression.Instance; - } - - if (instance is null) - { - return null; - } - - var declaringType = member.DeclaringType; - - if (declaringType == typeof(LocalDateTime) - || declaringType == typeof(LocalDate) - || declaringType == typeof(LocalTime) - || declaringType == typeof(Period)) - { - return TranslateDateTime(instance, member); - } - - if (declaringType == typeof(ZonedDateTime)) - { - return TranslateZonedDateTime(instance, member, returnType); - } - - if (declaringType == typeof(Duration)) - { - return TranslateDuration(instance, member); - } - - if (declaringType == typeof(Interval)) - { - return TranslateInterval(instance, member); - } - - if (declaringType == typeof(DateInterval)) - { - return TranslateDateInterval(instance, member); - } - - return null; - } - - private SqlExpression? TranslateDuration(SqlExpression instance, MemberInfo member) - { - return member.Name switch - { - nameof(Duration.TotalDays) => TranslateDurationTotalMember(instance, 86400), - nameof(Duration.TotalHours) => TranslateDurationTotalMember(instance, 3600), - nameof(Duration.TotalMinutes) => TranslateDurationTotalMember(instance, 60), - nameof(Duration.TotalSeconds) => GetDatePartExpressionDouble(instance, "epoch"), - nameof(Duration.TotalMilliseconds) => TranslateDurationTotalMember(instance, 0.001), - nameof(Duration.Days) => GetDatePartExpression(instance, "day"), - nameof(Duration.Hours) => GetDatePartExpression(instance, "hour"), - nameof(Duration.Minutes) => GetDatePartExpression(instance, "minute"), - nameof(Duration.Seconds) => GetDatePartExpression(instance, "second", true), - nameof(Duration.Milliseconds) => null, // Too annoying, floating point and sub-millisecond handling - _ => null, - }; - - SqlExpression TranslateDurationTotalMember(SqlExpression instance, double divisor) - => _sqlExpressionFactory.Divide(GetDatePartExpressionDouble(instance, "epoch"), _sqlExpressionFactory.Constant(divisor)); - } - - private SqlExpression? TranslateInterval(SqlExpression instance, MemberInfo member) - { - if (member == Interval_Start) - { - return Lower(); - } - - if (member == Interval_End) - { - return Upper(); - } - - if (member == Interval_HasStart) - { - return _sqlExpressionFactory.Not( - _sqlExpressionFactory.Function( - "lower_inf", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(bool))); - } - - if (member == Interval_HasEnd) - { - return _sqlExpressionFactory.Not( - _sqlExpressionFactory.Function( - "upper_inf", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(bool))); - } - - if (member == Interval_Duration) - { - return _sqlExpressionFactory.Subtract(Upper(), Lower(), _typeMappingSource.FindMapping(typeof(Duration))); - } - - return null; - - SqlExpression Lower() - => _sqlExpressionFactory.Function( - "lower", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(Interval), - _typeMappingSource.FindMapping(typeof(Instant))); - - SqlExpression Upper() - => _sqlExpressionFactory.Function( - "upper", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(Interval), - _typeMappingSource.FindMapping(typeof(Instant))); - } - - private SqlExpression? TranslateDateInterval(SqlExpression instance, MemberInfo member) - { - // NodaTime DateInterval is inclusive on both ends. - // PostgreSQL daterange is a discrete range type; this means it gets normalized to inclusive lower bound, exclusive upper bound. - // So we can translate Start as-is, but need to subtract a day for End. - if (member == DateInterval_Start) - { - return Lower(); - } - - if (member == DateInterval_End) - { - // 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)), typeof(LocalDate), - _typeMappingSource.FindMapping(typeof(LocalDate))); - } - - if (member == DateInterval_Length) - { - return _sqlExpressionFactory.Subtract(Upper(), Lower()); - } - - return null; - - SqlExpression Lower() - => _sqlExpressionFactory.Function( - "lower", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(LocalDate), - _dateTypeMapping); - - SqlExpression Upper() - => _sqlExpressionFactory.Function( - "upper", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(LocalDate), - _dateTypeMapping); - } - - private SqlExpression? TranslateDateTime(SqlExpression instance, MemberInfo member) - => member.Name switch - { - "Year" or "Years" => GetDatePartExpression(instance, "year"), - "Month" or "Months" => GetDatePartExpression(instance, "month"), - "DayOfYear" => GetDatePartExpression(instance, "doy"), - "Day" or "Days" => GetDatePartExpression(instance, "day"), - "Hour" or "Hours" => GetDatePartExpression(instance, "hour"), - "Minute" or "Minutes" => GetDatePartExpression(instance, "minute"), - "Second" or "Seconds" => GetDatePartExpression(instance, "second", true), - "Millisecond" or "Milliseconds" => null, // Too annoying - - // Unlike DateTime.DayOfWeek, NodaTime's IsoDayOfWeek enum doesn't exactly correspond to PostgreSQL's - // values returned by date_part('dow', ...): in NodaTime Sunday is 7 and not 0, which is None. - // So we generate a CASE WHEN expression to translate PostgreSQL's 0 to 7. - "DayOfWeek" when GetDatePartExpression(instance, "dow", true) is var getValueExpression - => _sqlExpressionFactory.Case( - getValueExpression, - [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 - // conversion (based on TimeZone), which we don't want (so avoid translating except on timestamp). - // The translation for ZonedDateTime.Date converts to timestamp before ending up here. - "Date" when instance.TypeMapping is TimestampLocalDateTimeMapping or LegacyTimestampInstantMapping - => _sqlExpressionFactory.Convert(instance, typeof(LocalDate), _typeMappingSource.FindMapping(typeof(LocalDate))!), - - "TimeOfDay" => _sqlExpressionFactory.Convert( - instance, - typeof(LocalTime), - _typeMappingSource.FindMapping(typeof(LocalTime), storeTypeName: "time")), - - _ => null - }; - - /// - /// Constructs the date_part expression. - /// - /// The expression. - /// The name of the date_part to construct. - /// True if the result should be wrapped with floor(...); otherwise, false. - /// - /// The date_part expression. - /// - /// - /// date_part returns doubles, which we floor and cast into ints - /// This also gets rid of sub-second components when retrieving seconds. - /// - private SqlExpression GetDatePartExpression( - SqlExpression instance, - string partName, - bool floor = false) - { - var result = GetDatePartExpressionDouble(instance, partName, floor); - return _sqlExpressionFactory.Convert(result, typeof(int)); - } - - private SqlExpression GetDatePartExpressionDouble( - SqlExpression instance, - string partName, - bool floor = false) - { - var result = _sqlExpressionFactory.Function( - "date_part", - [_sqlExpressionFactory.Constant(partName), instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(double)); - - if (floor) - { - result = _sqlExpressionFactory.Function( - "floor", - [result], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(double)); - } - - return result; - } - - private SqlExpression? TranslateZonedDateTime(SqlExpression instance, MemberInfo member, Type returnType) - { - if (instance is PendingZonedDateTimeExpression pendingZonedDateTime) - { - instance = _sqlExpressionFactory.AtTimeZone( - pendingZonedDateTime.Operand, - pendingZonedDateTime.TimeZoneId, - typeof(LocalDateTime), - _localDateTimeTypeMapping); - - return member == ZonedDateTime_LocalDateTime - ? instance - : TranslateDateTime(instance, member); - } - - // date_part, which is used to extract most components, doesn't have an overload for timestamptz, so passing one directly - // converts it to the local timezone as per TimeZone. Explicitly convert it to a 'timestamp without time zone' in UTC. - // The same works also for the LocalDateTime member. - instance = _sqlExpressionFactory.AtUtc(instance); - - return member == ZonedDateTime_LocalDateTime - ? instance - : TranslateDateTime(instance, member); - } -} diff --git a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs b/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs deleted file mode 100644 index 4d9273a3df..0000000000 --- a/src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs +++ /dev/null @@ -1,426 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Query.Internal; - -/// -/// Provides translation services for members. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-datetime.html -/// -public class NpgsqlNodaTimeMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin -{ - /// - /// 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 NpgsqlNodaTimeMethodCallTranslatorPlugin( - IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) - { - Translators = - [ - new NpgsqlNodaTimeMethodCallTranslator(typeMappingSource, (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 virtual IEnumerable Translators { 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 class NpgsqlNodaTimeMethodCallTranslator : IMethodCallTranslator -{ - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - private static readonly MethodInfo SystemClock_GetCurrentInstant = - typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), Type.EmptyTypes)!; - - private static readonly MethodInfo Instant_InUtc = - typeof(Instant).GetRuntimeMethod(nameof(Instant.InUtc), Type.EmptyTypes)!; - - private static readonly MethodInfo Instant_InZone = - 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), [typeof(DbFunctions), typeof(Instant), typeof(Instant)])!; - - private static readonly MethodInfo ZonedDateTime_ToInstant = - typeof(ZonedDateTime).GetRuntimeMethod(nameof(ZonedDateTime.ToInstant), Type.EmptyTypes)!; - - private static readonly MethodInfo ZonedDateTime_Distance = - typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), - [typeof(DbFunctions), typeof(ZonedDateTime), typeof(ZonedDateTime)])!; - - private static readonly MethodInfo LocalDateTime_InZoneLeniently = - typeof(LocalDateTime).GetRuntimeMethod(nameof(LocalDateTime.InZoneLeniently), [typeof(DateTimeZone)])!; - - private static readonly MethodInfo LocalDateTime_Distance = - typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), - [typeof(DbFunctions), typeof(LocalDateTime), typeof(LocalDateTime)])!; - - private static readonly MethodInfo LocalDate_Distance = - typeof(NpgsqlNodaTimeDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlNodaTimeDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(LocalDate), typeof(LocalDate)])!; - - 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), [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), [typeof(long)])!; - - private static readonly MethodInfo Period_FromMinutes = - typeof(Period).GetRuntimeMethod(nameof(Period.FromMinutes), [typeof(long)])!; - - private static readonly MethodInfo Period_FromSeconds = - typeof(Period).GetRuntimeMethod(nameof(Period.FromSeconds), [typeof(long)])!; - - private static readonly MethodInfo Interval_Contains - = typeof(Interval).GetRuntimeMethod(nameof(Interval.Contains), [typeof(Instant)])!; - - private static readonly MethodInfo DateInterval_Contains_LocalDate - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), [typeof(LocalDate)])!; - - private static readonly MethodInfo DateInterval_Contains_DateInterval - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Contains), [typeof(DateInterval)])!; - - private static readonly MethodInfo DateInterval_Intersection - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Intersection), [typeof(DateInterval)])!; - - private static readonly MethodInfo DateInterval_Union - = typeof(DateInterval).GetRuntimeMethod(nameof(DateInterval.Union), [typeof(DateInterval)])!; - - private static readonly MethodInfo IDateTimeZoneProvider_get_Item - = typeof(IDateTimeZoneProvider).GetRuntimeMethod("get_Item", [typeof(string)])!; - - private static readonly bool[][] TrueArrays = [[], [true], [true, 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. - /// - public NpgsqlNodaTimeMethodCallTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - } - -#pragma warning disable EF1001 - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - var translated = - TranslateInstant(instance, method, arguments) - ?? TranslateZonedDateTime(instance, method, arguments) - ?? TranslateLocalDateTime(instance, method, arguments) - ?? TranslateLocalDate(instance, method, arguments) - ?? TranslatePeriod(instance, method, arguments, logger) - ?? TranslateInterval(instance, method, arguments, logger) - ?? TranslateDateInterval(instance, method, arguments, logger); - - if (translated is not null) - { - return translated; - } - - if (method == IDateTimeZoneProvider_get_Item && instance is PendingDateTimeZoneProviderExpression) - { - // We're translating an expression such as 'DateTimeZoneProviders.Tzdb["Europe/Berlin"]'. - // Note that the .NET type of that expression is DateTimeZone, but we just return the string ID for the time zone. - return arguments[0]; - } - - return null; - } - - private SqlExpression? TranslateInstant( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (method == SystemClock_GetCurrentInstant) - { - return NpgsqlNodaTimeTypeMappingSourcePlugin.LegacyTimestampBehavior - ? _sqlExpressionFactory.AtTimeZone( - _sqlExpressionFactory.Function( - "NOW", - [], - nullable: false, - argumentsPropagateNullability: [], - method.ReturnType), - _sqlExpressionFactory.Constant("UTC"), - method.ReturnType) - : _sqlExpressionFactory.Function( - "NOW", - [], - nullable: false, - argumentsPropagateNullability: [], - method.ReturnType, - _typeMappingSource.FindMapping(typeof(Instant), "timestamp with time zone")); - } - - if (method == Instant_InUtc) - { - // Instant -> ZonedDateTime is a no-op (different types in .NET but both mapped to timestamptz in PG) - return instance; - } - - if (method == Instant_InZone) - { - // When InZone is called, we have a mismatch: on the .NET NodaTime side, we have a ZonedDateTime; but on the PostgreSQL side, - // the AT TIME ZONE expression returns a 'timestamp without time zone' (when applied to a 'timestamp with time zone', which is - // what ZonedDateTime is mapped to). - return new PendingZonedDateTimeExpression(instance!, arguments[0]); - } - - if (method == Instant_ToDateTimeUtc) - { - return _sqlExpressionFactory.Convert( - instance!, - typeof(DateTime), - _typeMappingSource.FindMapping(typeof(DateTime), "timestamp with time zone")); - } - - if (method == Instant_Distance) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.Distance, arguments[1], arguments[2]); - } - - return null; - } - - private SqlExpression? TranslateZonedDateTime( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (method == ZonedDateTime_ToInstant) - { - // We get here with the expression localDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).ToInstant() - if (instance is PendingZonedDateTimeExpression pendingZonedDateTime) - { - return _sqlExpressionFactory.AtTimeZone( - pendingZonedDateTime.Operand, - pendingZonedDateTime.TimeZoneId, - typeof(Instant), - _typeMappingSource.FindMapping(typeof(Instant))); - } - - // Otherwise, ZonedDateTime -> ToInstant is a no-op (different types in .NET but both mapped to timestamptz in PG) - return instance; - } - - if (method == ZonedDateTime_Distance) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.Distance, arguments[1], arguments[2]); - } - - return null; - } - - private SqlExpression? TranslateLocalDateTime( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (method == LocalDateTime_InZoneLeniently) - { - return new PendingZonedDateTimeExpression(instance!, arguments[0]); - } - - if (method == LocalDateTime_Distance) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.Distance, arguments[1], arguments[2]); - } - - return null; - } - - private SqlExpression? TranslateLocalDate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (method == LocalDate_Distance) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.Distance, arguments[1], arguments[2]); - } - - if (method.DeclaringType == typeof(LocalDate)) - { - return method.Name switch - { - nameof(LocalDate.At) => new SqlBinaryExpression( - ExpressionType.Add, - _sqlExpressionFactory.ApplyDefaultTypeMapping(instance!), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), - typeof(LocalDateTime), - _typeMappingSource.FindMapping(typeof(LocalDateTime))), - - nameof(LocalDate.AtMidnight) => new SqlBinaryExpression( - ExpressionType.Add, - _sqlExpressionFactory.ApplyDefaultTypeMapping(instance!), - new SqlConstantExpression(new LocalTime(0, 0, 0), _typeMappingSource.FindMapping(typeof(LocalTime))), - typeof(LocalDateTime), - _typeMappingSource.FindMapping(typeof(LocalDateTime))), - - _ => null - }; - } - return null; - } - - private SqlExpression? TranslatePeriod( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType != typeof(Period)) - { - return null; - } - - if (method == Period_FromYears) - { - return IntervalPart("years", arguments[0]); - } - - if (method == Period_FromMonths) - { - return IntervalPart("months", arguments[0]); - } - - if (method == Period_FromWeeks) - { - return IntervalPart("weeks", arguments[0]); - } - - if (method == Period_FromDays) - { - return IntervalPart("days", arguments[0]); - } - - if (method == Period_FromHours) - { - return IntervalPartOverBigInt("hours", arguments[0]); - } - - if (method == Period_FromMinutes) - { - return IntervalPartOverBigInt("mins", arguments[0]); - } - - if (method == Period_FromSeconds) - { - return IntervalPart( - "secs", _sqlExpressionFactory.Convert(arguments[0], typeof(double), _typeMappingSource.FindMapping(typeof(double)))); - } - - return null; - - static PgFunctionExpression IntervalPart(string datePart, SqlExpression parameter) - => PgFunctionExpression.CreateWithNamedArguments( - "make_interval", - [parameter], - [datePart], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - builtIn: true, - typeof(Period), - typeMapping: null); - - PgFunctionExpression IntervalPartOverBigInt(string datePart, SqlExpression parameter) - { - parameter = _sqlExpressionFactory.ApplyDefaultTypeMapping(parameter); - - // NodaTime Period.FromHours/Minutes/Seconds accept a long parameter, but PG interval_part accepts an int. - // If the parameter happens to be an int cast up to a long, just unwrap it, otherwise downcast from bigint to int - // (this will throw on the PG side if the bigint is out of int range) - if (parameter is SqlUnaryExpression { OperatorType: ExpressionType.Convert } convertExpression - && convertExpression.TypeMapping!.StoreType == "bigint" - && convertExpression.Operand.TypeMapping!.StoreType == "integer") - { - return IntervalPart(datePart, convertExpression.Operand); - } - - return IntervalPart( - datePart, _sqlExpressionFactory.Convert(parameter, typeof(int), _typeMappingSource.FindMapping(typeof(int)))); - } - } - - private SqlExpression? TranslateInterval( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method == Interval_Contains) - { - return _sqlExpressionFactory.Contains(instance!, arguments[0]); - } - - return null; - } - - private SqlExpression? TranslateDateInterval( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method == DateInterval_Contains_LocalDate - || method == DateInterval_Contains_DateInterval) - { - return _sqlExpressionFactory.Contains(instance!, arguments[0]); - } - - if (method == DateInterval_Intersection) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeIntersect, instance!, arguments[0]); - } - - if (method == DateInterval_Union) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeUnion, instance!, arguments[0]); - } - - return null; - } -#pragma warning restore EF1001 -} diff --git a/src/EFCore.PG.NodaTime/README.md b/src/EFCore.PG.NodaTime/README.md deleted file mode 100644 index 2372bc2a6c..0000000000 --- a/src/EFCore.PG.NodaTime/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Npgsql Entity Framework Core provider for PostgreSQL - -Npgsql.EntityFrameworkCore.PostgreSQL is the open source EF Core provider for PostgreSQL. It allows you to interact with PostgreSQL via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. - -This package is a plugin which allows you to use the [NodaTime](https://nodatime.org) date/time library when interacting with PostgreSQL; this provides a better and safer API for dealing with date and time data. - -To use the plugin, simply add `UseNodaTime` as below and use NodaTime types in your entity properties: - -```csharp -await using var ctx = new BlogContext(); -await ctx.Database.EnsureDeletedAsync(); -await ctx.Database.EnsureCreatedAsync(); - -// Insert a Blog -ctx.Blogs.Add(new() -{ - Name = "FooBlog", - CreationTime = SystemClock.Instance.GetCurrentInstant() -}); -await ctx.SaveChangesAsync(); - -// Query all blogs created in 2020 or after -var newBlogs = await ctx.Blogs.Where(b => b.CreationTime >= Instant.FromUtc(2020, 1, 1, 0, 0, 0)).ToListAsync(); - -public class BlogContext : DbContext -{ - public DbSet Blogs { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql( - @"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase", - o => o.UseNodaTime()); -} - -public class Blog -{ - public int Id { get; set; } - public string Name { get; set; } - public Instant CreationTime { get; set; } -} -``` - -The plugin also supports translating most NodaTime methods and properties into corresponding PostgreSQL date/time operations. For more information, see the [NodaTime plugin documentation page](https://www.npgsql.org/efcore/mapping/nodatime.html). diff --git a/src/EFCore.PG.NodaTime/Scaffolding/Internal/NpgsqlNodaTimeCodeGeneratorPlugin.cs b/src/EFCore.PG.NodaTime/Scaffolding/Internal/NpgsqlNodaTimeCodeGeneratorPlugin.cs deleted file mode 100644 index 78e9cb8377..0000000000 --- a/src/EFCore.PG.NodaTime/Scaffolding/Internal/NpgsqlNodaTimeCodeGeneratorPlugin.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Scaffolding.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 NpgsqlNodaTimeCodeGeneratorPlugin : ProviderCodeGeneratorPlugin -{ - private static readonly MethodInfo _useNodaTimeMethodInfo - = typeof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime), - typeof(NpgsqlDbContextOptionsBuilder)); - - /// - /// 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 MethodCallCodeFragment GenerateProviderOptions() - => new(_useNodaTimeMethodInfo); -} diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs deleted file mode 100644 index d3a581c8e7..0000000000 --- a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System.Collections.Concurrent; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin -{ -#if DEBUG - internal static bool LegacyTimestampBehavior; - internal static bool DisableDateTimeInfinityConversions; -#else - internal static readonly bool LegacyTimestampBehavior; - internal static readonly bool DisableDateTimeInfinityConversions; -#endif - - static NpgsqlNodaTimeTypeMappingSourcePlugin() - { - LegacyTimestampBehavior = AppContext.TryGetSwitch("Npgsql.EnableLegacyTimestampBehavior", out var enabled) && enabled; - DisableDateTimeInfinityConversions = AppContext.TryGetSwitch("Npgsql.DisableDateTimeInfinityConversions", out enabled) && enabled; - } - - /// - /// 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 ConcurrentDictionary StoreTypeMappings { 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 ConcurrentDictionary ClrTypeMappings { get; } - - #region TypeMapping - - private readonly TimestampLocalDateTimeMapping _timestampLocalDateTime = TimestampLocalDateTimeMapping.Default; - private readonly LegacyTimestampInstantMapping _legacyTimestampInstant = LegacyTimestampInstantMapping.Default; - - private readonly TimestampTzInstantMapping _timestamptzInstant = TimestampTzInstantMapping.Default; - private readonly TimestampTzZonedDateTimeMapping _timestamptzZonedDateTime = TimestampTzZonedDateTimeMapping.Default; - private readonly TimestampTzOffsetDateTimeMapping _timestamptzOffsetDateTime = TimestampTzOffsetDateTimeMapping.Default; - - private readonly DateMapping _date = DateMapping.Default; - private readonly TimeMapping _time = TimeMapping.Default; - private readonly TimeTzMapping _timetz = TimeTzMapping.Default; - private readonly PeriodIntervalMapping _periodInterval = PeriodIntervalMapping.Default; - private readonly DurationIntervalMapping _durationInterval = DurationIntervalMapping.Default; - - // PostgreSQL has no native type for representing time zones - it just uses the IANA ID as text. - private readonly DateTimeZoneMapping _timeZone = new("text"); - - // Built-in ranges - private readonly NpgsqlRangeTypeMapping _timestampLocalDateTimeRange; - private readonly NpgsqlRangeTypeMapping _legacyTimestampInstantRange; - private readonly NpgsqlRangeTypeMapping _timestamptzInstantRange; - private readonly NpgsqlRangeTypeMapping _timestamptzZonedDateTimeRange; - private readonly NpgsqlRangeTypeMapping _timestamptzOffsetDateTimeRange; - private readonly NpgsqlRangeTypeMapping _dateRange; - private readonly DateIntervalRangeMapping _dateIntervalRange = new(); - private readonly IntervalRangeMapping _intervalRange = new(); - - #endregion - - /// - /// Constructs an instance of the class. - /// - public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationHelper) - { - _timestampLocalDateTimeRange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "tsrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampRange, _timestampLocalDateTime); - _legacyTimestampInstantRange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "tsrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampRange, _legacyTimestampInstant); - _timestamptzInstantRange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "tstzrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampTzRange, _timestamptzInstant); - _timestamptzZonedDateTimeRange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "tstzrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampTzRange, _timestamptzZonedDateTime); - _timestamptzOffsetDateTimeRange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "tstzrange", typeof(NpgsqlRange), NpgsqlDbType.TimestampTzRange, _timestamptzOffsetDateTime); - _dateRange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "daterange", typeof(NpgsqlRange), NpgsqlDbType.DateRange, _date); - - var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { - // We currently allow _legacyTimestampInstant even in non-legacy mode, since when upgrading to 6.0 with existing - // 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 - ? [_legacyTimestampInstant, _timestampLocalDateTime] - : [_timestampLocalDateTime, _legacyTimestampInstant] - }, - { - "timestamp with time zone", [_timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime] - }, - { "date", [_date] }, - { "time without time zone", [_time] }, - { "time with time zone", [_timetz] }, - { "interval", [_periodInterval, _durationInterval] }, - { - "tsrange", LegacyTimestampBehavior - ? [_legacyTimestampInstantRange, _timestampLocalDateTimeRange] - : [_timestampLocalDateTimeRange, _legacyTimestampInstantRange] - }, - { - "tstzrange", [ - _intervalRange, _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange - ] - }, - { "daterange", [_dateIntervalRange, _dateRange] } - }; - - // Set up aliases - storeTypeMappings["timestamp"] = storeTypeMappings["timestamp without time zone"]; - storeTypeMappings["timestamptz"] = storeTypeMappings["timestamp with time zone"]; - storeTypeMappings["time"] = storeTypeMappings["time without time zone"]; - storeTypeMappings["timetz"] = storeTypeMappings["time with time zone"]; - - var clrTypeMappings = new Dictionary - { - { typeof(Instant), LegacyTimestampBehavior ? _legacyTimestampInstant : _timestamptzInstant }, - { typeof(LocalDateTime), _timestampLocalDateTime }, - { typeof(ZonedDateTime), _timestamptzZonedDateTime }, - { typeof(OffsetDateTime), _timestamptzOffsetDateTime }, - { typeof(LocalDate), _date }, - { typeof(LocalTime), _time }, - { typeof(OffsetTime), _timetz }, - { typeof(Period), _periodInterval }, - { typeof(Duration), _durationInterval }, - // See DateTimeZone below - - { typeof(NpgsqlRange), LegacyTimestampBehavior ? _legacyTimestampInstantRange : _timestamptzInstantRange }, - { typeof(NpgsqlRange), _timestampLocalDateTimeRange }, - { typeof(NpgsqlRange), _timestamptzZonedDateTimeRange }, - { typeof(NpgsqlRange), _timestamptzOffsetDateTimeRange }, - { typeof(NpgsqlRange), _dateRange }, - { typeof(DateInterval), _dateIntervalRange }, - { typeof(Interval), _intervalRange }, - }; - - StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); - ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); - } - - /// - /// 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 RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) - => FindBaseMapping(mappingInfo)?.Clone(mappingInfo); - - /// - /// 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? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo) - { - var clrType = mappingInfo.ClrType; - var storeTypeName = mappingInfo.StoreTypeName; - var storeTypeNameBase = mappingInfo.StoreTypeNameBase; - - if (storeTypeName is not null) - { - if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings)) - { - if (clrType is null) - { - return mappings[0]; - } - - foreach (var m in mappings) - { - if (m.ClrType == clrType) - { - return m; - } - } - - return null; - } - - if (StoreTypeMappings.TryGetValue(storeTypeNameBase!, out mappings)) - { - if (clrType is null) - { - return mappings[0]; - } - - foreach (var m in mappings) - { - if (m.ClrType == clrType) - { - return m; - } - } - - return null; - } - } - - if (clrType is not null) - { - if (ClrTypeMappings.TryGetValue(clrType, out var mapping)) - { - return mapping; - } - - if (clrType.IsAssignableTo(typeof(DateTimeZone))) - { - return _timeZone; - } - } - - return null; - } -} diff --git a/src/EFCore.PG.NodaTime/Utilties/Util.cs b/src/EFCore.PG.NodaTime/Utilties/Util.cs deleted file mode 100644 index 6461a9367c..0000000000 --- a/src/EFCore.PG.NodaTime/Utilties/Util.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties; - -internal static class Util -{ - internal static NewExpression ConstantNew(ConstructorInfo constructor, params object[] parameters) - => Expression.New(constructor, parameters.Select(p => Expression.Constant(p)).ToArray()); - - internal static MethodCallExpression ConstantCall(MethodInfo method, params object[] parameters) - => Expression.Call(method, parameters.Select(p => Expression.Constant(p)).ToArray()); -} diff --git a/src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets b/src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets deleted file mode 100644 index 2bbe4bc5d9..0000000000 --- a/src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets +++ /dev/null @@ -1,46 +0,0 @@ - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - $(IntermediateOutputPath)EFCoreNpgsqlNodaTime$(DefaultLanguageSourceExtension) - - - - - - - CompileBefore - - - - - CompileAfter - - - - - - - Compile - - - - - - - <_Parameter1>Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal.NpgsqlNodaTimeDesignTimeServices, Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime - <_Parameter2>Npgsql.EntityFrameworkCore.PostgreSQL - - - - - - - - diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs deleted file mode 100644 index 40d5f17dfd..0000000000 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ /dev/null @@ -1,447 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.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 NpgsqlAnnotationCodeGenerator : AnnotationCodeGenerator -{ - #region MethodInfos - - private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo1 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string)); - - private static readonly MethodInfo ModelHasPostgresExtensionMethodInfo2 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension), typeof(ModelBuilder), typeof(string), typeof(string), - typeof(string)); - - private static readonly MethodInfo ModelHasPostgresEnumMethodInfo1 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string[])); - - private static readonly MethodInfo ModelHasPostgresEnumMethodInfo2 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string[])); - - private static readonly MethodInfo ModelHasPostgresRangeMethodInfo1 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string)); - - private static readonly MethodInfo ModelHasPostgresRangeMethodInfo2 - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string), - typeof(string), typeof(string), typeof(string), typeof(string)); - - private static readonly MethodInfo ModelUseSerialColumnsMethodInfo - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.UseSerialColumns), typeof(ModelBuilder)); - - private static readonly MethodInfo ModelUseIdentityAlwaysColumnsMethodInfo - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.UseIdentityAlwaysColumns), typeof(ModelBuilder)); - - private static readonly MethodInfo ModelUseIdentityByDefaultColumnsMethodInfo - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns), typeof(ModelBuilder)); - - private static readonly MethodInfo ModelUseHiLoMethodInfo - = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.UseHiLo), typeof(ModelBuilder), typeof(string), typeof(string)); - - private static readonly MethodInfo ModelHasAnnotationMethodInfo - = typeof(ModelBuilder).GetRequiredRuntimeMethod( - nameof(ModelBuilder.HasAnnotation), typeof(string), typeof(object)); - - private static readonly MethodInfo ModelUseKeySequencesMethodInfo - = typeof(NpgsqlModelBuilderExtensions).GetRuntimeMethod( - nameof(NpgsqlModelBuilderExtensions.UseKeySequences), [typeof(ModelBuilder), typeof(string), typeof(string)])!; - - private static readonly MethodInfo EntityTypeIsUnloggedMethodInfo - = typeof(NpgsqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlEntityTypeBuilderExtensions.IsUnlogged), typeof(EntityTypeBuilder), typeof(bool)); - - private static readonly MethodInfo PropertyUseSerialColumnMethodInfo - = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.UseSerialColumn), typeof(PropertyBuilder)); - - private static readonly MethodInfo PropertyUseIdentityAlwaysColumnMethodInfo - = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn), typeof(PropertyBuilder)); - - private static readonly MethodInfo PropertyUseIdentityByDefaultColumnMethodInfo - = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn), typeof(PropertyBuilder)); - - private static readonly MethodInfo PropertyUseHiLoMethodInfo - = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.UseHiLo), typeof(PropertyBuilder), typeof(string), typeof(string)); - - private static readonly MethodInfo PropertyHasIdentityOptionsMethodInfo - = typeof(NpgsqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.HasIdentityOptions), typeof(PropertyBuilder), typeof(long?), typeof(long?), - typeof(long?), typeof(long?), typeof(bool?), typeof(long?)); - - private static readonly MethodInfo PropertyUseSequenceMethodInfo - = typeof(NpgsqlPropertyBuilderExtensions).GetRuntimeMethod( - nameof(NpgsqlPropertyBuilderExtensions.UseSequence), [typeof(PropertyBuilder), typeof(string), typeof(string)])!; - - private static readonly MethodInfo IndexUseCollationMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.UseCollation), typeof(IndexBuilder), typeof(string[])); - - private static readonly MethodInfo IndexHasMethodMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.HasMethod), typeof(IndexBuilder), typeof(string)); - - private static readonly MethodInfo IndexHasOperatorsMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.HasOperators), typeof(IndexBuilder), typeof(string[])); - - private static readonly MethodInfo IndexHasNullSortOrderMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.HasNullSortOrder), typeof(IndexBuilder), typeof(NullSortOrder[])); - - private static readonly MethodInfo IndexIncludePropertiesMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.IncludeProperties), typeof(IndexBuilder), typeof(string[])); - - private static readonly MethodInfo IndexAreNullsDistinctMethodInfo - = typeof(NpgsqlIndexBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlIndexBuilderExtensions.AreNullsDistinct), typeof(IndexBuilder), typeof(bool)); - - #endregion MethodInfos - - /// - /// 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 NpgsqlAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies) - : base(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 - /// 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 IsHandledByConvention(IModel model, IAnnotation annotation) - { - Check.NotNull(model, nameof(model)); - Check.NotNull(annotation, nameof(annotation)); - - if (annotation.Name == RelationalAnnotationNames.DefaultSchema - && (string?)annotation.Value == "public") - { - return true; - } - - return false; - } - - /// - /// 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 IsHandledByConvention(IIndex index, IAnnotation annotation) - { - Check.NotNull(index, nameof(index)); - Check.NotNull(annotation, nameof(annotation)); - - if (annotation.Name == NpgsqlAnnotationNames.IndexMethod - && (string?)annotation.Value == "btree") - { - return true; - } - - return false; - } - - /// - /// 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 IsHandledByConvention(IProperty property, IAnnotation annotation) - { - Check.NotNull(property, nameof(property)); - Check.NotNull(annotation, nameof(annotation)); - - // The default by-convention value generation strategy is serial in pre-10 PostgreSQL, - // and IdentityByDefault otherwise. - if (annotation.Name == NpgsqlAnnotationNames.ValueGenerationStrategy) - { - // Note: both serial and identity-by-default columns are considered by-convention - we don't want - // to assume that the PostgreSQL version of the scaffolded database necessarily determines the - // version of the database that the scaffolded model will target. This makes life difficult for - // models with mixed strategies but that's an edge case. - return (NpgsqlValueGenerationStrategy?)annotation.Value switch - { - NpgsqlValueGenerationStrategy.SerialColumn => true, - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn => true, - _ => false - }; - } - - return false; - } - - /// - /// 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 IReadOnlyList GenerateFluentApiCalls( - IModel model, - IDictionary annotations) - { - var fragments = new List(base.GenerateFluentApiCalls(model, annotations)); - - if (GenerateValueGenerationStrategy(annotations, onModel: true) is { } valueGenerationStrategy) - { - fragments.Add(valueGenerationStrategy); - } - - return fragments; - } - - /// - /// 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 MethodCallCodeFragment? GenerateFluentApi(IModel model, IAnnotation annotation) - { - Check.NotNull(model, nameof(model)); - Check.NotNull(annotation, nameof(annotation)); - - if (annotation.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)) - { - var extension = new PostgresExtension(model, annotation.Name); - - return extension.Schema is "public" or null - ? new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo1, extension.Name) - : new MethodCallCodeFragment(ModelHasPostgresExtensionMethodInfo2, extension.Schema, extension.Name); - } - - if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal)) - { - var enumTypeDef = new PostgresEnum(model, annotation.Name); - - return enumTypeDef.Schema is null - ? new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo1, enumTypeDef.Name, enumTypeDef.Labels) - : new MethodCallCodeFragment(ModelHasPostgresEnumMethodInfo2, enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); - } - - if (annotation.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal)) - { - var rangeTypeDef = new PostgresRange(model, annotation.Name); - - if (rangeTypeDef.Schema is null - && rangeTypeDef.CanonicalFunction is null - && rangeTypeDef.SubtypeOpClass is null - && rangeTypeDef.Collation is null - && rangeTypeDef.SubtypeDiff is null) - { - return new MethodCallCodeFragment(ModelHasPostgresRangeMethodInfo1, rangeTypeDef.Name, rangeTypeDef.Subtype); - } - - return new MethodCallCodeFragment( - ModelHasPostgresRangeMethodInfo2, - rangeTypeDef.Schema, - rangeTypeDef.Name, - rangeTypeDef.Subtype, - rangeTypeDef.CanonicalFunction, - rangeTypeDef.SubtypeOpClass, - rangeTypeDef.Collation, - rangeTypeDef.SubtypeDiff); - } - - return 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 override MethodCallCodeFragment? GenerateFluentApi(IEntityType entityType, IAnnotation annotation) - { - Check.NotNull(entityType, nameof(entityType)); - Check.NotNull(annotation, nameof(annotation)); - - if (annotation.Name == NpgsqlAnnotationNames.UnloggedTable) - { - return new MethodCallCodeFragment(EntityTypeIsUnloggedMethodInfo, annotation.Value); - } - - return 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 override IReadOnlyList GenerateFluentApiCalls( - IProperty property, - IDictionary annotations) - { - var fragments = new List(base.GenerateFluentApiCalls(property, annotations)); - - if (GenerateValueGenerationStrategy(annotations, onModel: false) is { } valueGenerationStrategy) - { - fragments.Add(valueGenerationStrategy); - } - - if (GenerateIdentityOptions(annotations) is { } identityOptionsFragment) - { - fragments.Add(identityOptionsFragment); - } - - return fragments; - } - - private MethodCallCodeFragment? GenerateValueGenerationStrategy(IDictionary annotations, bool onModel) - { - if (!TryGetAndRemove(annotations, NpgsqlAnnotationNames.ValueGenerationStrategy, out NpgsqlValueGenerationStrategy strategy)) - { - return null; - } - - switch (strategy) - { - case NpgsqlValueGenerationStrategy.SerialColumn: - return new MethodCallCodeFragment(onModel ? ModelUseSerialColumnsMethodInfo : PropertyUseSerialColumnMethodInfo); - - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - return new MethodCallCodeFragment( - onModel ? ModelUseIdentityAlwaysColumnsMethodInfo : PropertyUseIdentityAlwaysColumnMethodInfo); - - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - return new MethodCallCodeFragment( - onModel ? ModelUseIdentityByDefaultColumnsMethodInfo : PropertyUseIdentityByDefaultColumnMethodInfo); - - case NpgsqlValueGenerationStrategy.SequenceHiLo: - { - var name = GetAndRemove(NpgsqlAnnotationNames.HiLoSequenceName)!; - var schema = GetAndRemove(NpgsqlAnnotationNames.HiLoSequenceSchema); - return new MethodCallCodeFragment( - onModel ? ModelUseHiLoMethodInfo : PropertyUseHiLoMethodInfo, - (name, schema) switch - { - (null, null) => [], - (_, null) => [name], - _ => [name!, schema] - }); - } - - case NpgsqlValueGenerationStrategy.Sequence: - { - var nameOrSuffix = GetAndRemove( - onModel ? NpgsqlAnnotationNames.SequenceNameSuffix : NpgsqlAnnotationNames.SequenceName); - - var schema = GetAndRemove(NpgsqlAnnotationNames.SequenceSchema); - return new MethodCallCodeFragment( - onModel ? ModelUseKeySequencesMethodInfo : PropertyUseSequenceMethodInfo, - (name: nameOrSuffix, schema) switch - { - (null, null) => [], - (_, null) => [nameOrSuffix], - _ => [nameOrSuffix!, schema] - }); - } - case NpgsqlValueGenerationStrategy.None: - return new MethodCallCodeFragment( - ModelHasAnnotationMethodInfo, NpgsqlAnnotationNames.ValueGenerationStrategy, NpgsqlValueGenerationStrategy.None); - - default: - throw new ArgumentOutOfRangeException(strategy.ToString()); - } - - T? GetAndRemove(string annotationName) - => TryGetAndRemove(annotations, annotationName, out T? annotationValue) - ? annotationValue - : default; - } - - private MethodCallCodeFragment? GenerateIdentityOptions(IDictionary annotations) - { - if (!TryGetAndRemove( - annotations, NpgsqlAnnotationNames.IdentityOptions, - out string? annotationValue)) - { - return null; - } - - var identityOptions = IdentitySequenceOptionsData.Deserialize(annotationValue); - return new MethodCallCodeFragment( - PropertyHasIdentityOptionsMethodInfo, - identityOptions.StartValue, - identityOptions.IncrementBy == 1 ? null : (long?)identityOptions.IncrementBy, - identityOptions.MinValue, - identityOptions.MaxValue, - identityOptions.IsCyclic ? true : null, - identityOptions.NumbersToCache == 1 ? null : (long?)identityOptions.NumbersToCache); - } - - /// - /// 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 MethodCallCodeFragment? GenerateFluentApi(IIndex index, IAnnotation annotation) - => annotation.Name switch - { - RelationalAnnotationNames.Collation - => new MethodCallCodeFragment(IndexUseCollationMethodInfo, annotation.Value), - - NpgsqlAnnotationNames.IndexMethod - => new MethodCallCodeFragment(IndexHasMethodMethodInfo, annotation.Value), - NpgsqlAnnotationNames.IndexOperators - => new MethodCallCodeFragment(IndexHasOperatorsMethodInfo, annotation.Value), - NpgsqlAnnotationNames.IndexNullSortOrder - => new MethodCallCodeFragment(IndexHasNullSortOrderMethodInfo, annotation.Value), - NpgsqlAnnotationNames.IndexInclude - => new MethodCallCodeFragment(IndexIncludePropertiesMethodInfo, annotation.Value), - NpgsqlAnnotationNames.NullsDistinct - => new MethodCallCodeFragment(IndexAreNullsDistinctMethodInfo, annotation.Value), - _ => null - }; - - private static bool TryGetAndRemove( - IDictionary annotations, - string annotationName, - [NotNullWhen(true)] out T? annotationValue) - { - if (annotations.TryGetValue(annotationName, out var annotation) - && annotation.Value is not null) - { - annotations.Remove(annotationName); - annotationValue = (T)annotation.Value; - return true; - } - - annotationValue = default; - return false; - } -} diff --git a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs deleted file mode 100644 index e484cb56e7..0000000000 --- a/src/EFCore.PG/Design/Internal/NpgsqlCSharpRuntimeAnnotationCodeGenerator.cs +++ /dev/null @@ -1,378 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore.Design.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.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. -/// -#pragma warning disable EF1001 // Internal EF Core API usage. -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 - /// 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 NpgsqlCSharpRuntimeAnnotationCodeGenerator( - CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies, - RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } - - /// - /// 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 bool Create( - CoreTypeMapping typeMapping, - CSharpRuntimeAnnotationCodeGeneratorParameters parameters, - ValueComparer? valueComparer = null, - ValueComparer? keyValueComparer = null, - ValueComparer? providerValueComparer = null) - { - _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 - }; - - if (npgsqlDbTypeBasedDefaultInstance is not null) - { - CheckElementTypeMapping(); - - var npgsqlDbType = ((INpgsqlTypeMapping)typeMapping).NpgsqlDbType; - - if (npgsqlDbType != npgsqlDbTypeBasedDefaultInstance.NpgsqlDbType) - { - mainBuilder.AppendLine(";"); - - mainBuilder.Append( - $"{parameters.TargetName}.TypeMapping = (({typeMapping.GetType().Name}){parameters.TargetName}.TypeMapping).Clone(npgsqlDbType: "); - - mainBuilder - .Append(nameof(NpgsqlTypes)) - .Append(".") - .Append(nameof(NpgsqlDbType)) - .Append(".") - .Append(npgsqlDbType.ToString()); - - mainBuilder - .Append(")") - .DecrementIndent(); - } - - } - - switch (typeMapping) - { - case NpgsqlEnumTypeMapping enumTypeMapping: - 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) - { - mainBuilder - .Append('[') - .Append(code.UnknownLiteral(enumValue)) - .Append(']') - .Append(" = ") - .Append(code.Literal(label)) - .AppendLine(","); - } - - mainBuilder - .Append("}") - .DecrementIndent() - .Append(")") - .DecrementIndent(); - - break; - - case NpgsqlRangeTypeMapping rangeTypeMapping: - { - CheckElementTypeMapping(); - - var defaultInstance = NpgsqlRangeTypeMapping.Default; - - var npgsqlDbTypeDifferent = rangeTypeMapping.NpgsqlDbType != defaultInstance.NpgsqlDbType; - var subtypeTypeMappingIsDifferent = rangeTypeMapping.SubtypeMapping != defaultInstance.SubtypeMapping; - - if (npgsqlDbTypeDifferent || subtypeTypeMappingIsDifferent) - { - mainBuilder.AppendLine(";"); - - mainBuilder.AppendLine( - $"{parameters.TargetName}.TypeMapping = ((NpgsqlRangeTypeMapping){parameters.TargetName}.TypeMapping).Clone(") - .IncrementIndent(); - - mainBuilder - .Append("npgsqlDbType: ") - .Append(nameof(NpgsqlTypes)) - .Append(".") - .Append(nameof(NpgsqlDbType)) - .Append(".") - .Append(rangeTypeMapping.NpgsqlDbType.ToString()) - .AppendLine(","); - - mainBuilder.Append("subtypeTypeMapping: "); - - Create(rangeTypeMapping.SubtypeMapping, parameters); - - mainBuilder - .Append(")") - .DecrementIndent(); - } - - break; - } - } - - 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."); - } - } - } - - /// - public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - annotations.Remove(NpgsqlAnnotationNames.DatabaseTemplate); - annotations.Remove(NpgsqlAnnotationNames.Tablespace); - annotations.Remove(NpgsqlAnnotationNames.CollationDefinitionPrefix); - -#pragma warning disable CS0618 - annotations.Remove(NpgsqlAnnotationNames.DefaultColumnCollation); -#pragma warning restore CS0618 - - foreach (var annotationName in annotations.Keys.Where( - k => - k.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) - || k.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) - || k.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - - base.Generate(model, parameters); - } - - /// - public override void Generate(IRelationalModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - annotations.Remove(NpgsqlAnnotationNames.DatabaseTemplate); - annotations.Remove(NpgsqlAnnotationNames.Tablespace); - annotations.Remove(NpgsqlAnnotationNames.CollationDefinitionPrefix); - -#pragma warning disable CS0618 - annotations.Remove(NpgsqlAnnotationNames.DefaultColumnCollation); -#pragma warning restore CS0618 - - foreach (var annotationName in annotations.Keys.Where( - k => - k.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) - || k.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) - || k.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - - base.Generate(model, parameters); - } - - /// - public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - annotations.Remove(NpgsqlAnnotationNames.IdentityOptions); - annotations.Remove(NpgsqlAnnotationNames.TsVectorConfig); - annotations.Remove(NpgsqlAnnotationNames.TsVectorProperties); - annotations.Remove(NpgsqlAnnotationNames.CompressionMethod); - - if (!annotations.ContainsKey(NpgsqlAnnotationNames.ValueGenerationStrategy)) - { - annotations[NpgsqlAnnotationNames.ValueGenerationStrategy] = property.GetValueGenerationStrategy(); - } - } - - base.Generate(property, parameters); - } - - /// - public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - - annotations.Remove(NpgsqlAnnotationNames.IdentityOptions); - annotations.Remove(NpgsqlAnnotationNames.TsVectorConfig); - annotations.Remove(NpgsqlAnnotationNames.TsVectorProperties); - annotations.Remove(NpgsqlAnnotationNames.CompressionMethod); - } - - base.Generate(column, parameters); - } - - /// - public override void Generate(IIndex index, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - - annotations.Remove(NpgsqlAnnotationNames.IndexMethod); - annotations.Remove(NpgsqlAnnotationNames.IndexOperators); - annotations.Remove(NpgsqlAnnotationNames.IndexSortOrder); - annotations.Remove(NpgsqlAnnotationNames.IndexNullSortOrder); - annotations.Remove(NpgsqlAnnotationNames.IndexInclude); - annotations.Remove(NpgsqlAnnotationNames.CreatedConcurrently); - annotations.Remove(NpgsqlAnnotationNames.NullsDistinct); - - foreach (var annotationName in annotations.Keys.Where( - k => k.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - - base.Generate(index, parameters); - } - - /// - public override void Generate(ITableIndex index, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - - annotations.Remove(NpgsqlAnnotationNames.IndexMethod); - annotations.Remove(NpgsqlAnnotationNames.IndexOperators); - annotations.Remove(NpgsqlAnnotationNames.IndexSortOrder); - annotations.Remove(NpgsqlAnnotationNames.IndexNullSortOrder); - annotations.Remove(NpgsqlAnnotationNames.IndexInclude); - annotations.Remove(NpgsqlAnnotationNames.CreatedConcurrently); - annotations.Remove(NpgsqlAnnotationNames.NullsDistinct); - - foreach (var annotationName in annotations.Keys.Where( - k => k.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - - base.Generate(index, parameters); - } - - /// - public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - - annotations.Remove(NpgsqlAnnotationNames.UnloggedTable); - annotations.Remove(CockroachDbAnnotationNames.InterleaveInParent); - - foreach (var annotationName in annotations.Keys.Where( - k => k.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - - base.Generate(entityType, parameters); - } - - /// - public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - if (!parameters.IsRuntime) - { - var annotations = parameters.Annotations; - - annotations.Remove(NpgsqlAnnotationNames.UnloggedTable); - annotations.Remove(CockroachDbAnnotationNames.InterleaveInParent); - - foreach (var annotationName in annotations.Keys.Where( - k => k.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - - base.Generate(table, parameters); - } -} diff --git a/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs b/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs deleted file mode 100644 index 60cbeb104b..0000000000 --- a/src/EFCore.PG/Design/Internal/NpgsqlDesignTimeServices.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore.Design.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.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 NpgsqlDesignTimeServices : IDesignTimeServices -{ - /// - /// 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 void ConfigureDesignTimeServices(IServiceCollection serviceCollection) - { - Check.NotNull(serviceCollection, nameof(serviceCollection)); - - serviceCollection.AddEntityFrameworkNpgsql(); -#pragma warning disable EF1001 // Internal EF Core API usage. - new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) - .TryAdd() -#pragma warning restore EF1001 // Internal EF Core API usage. - .TryAdd() - .TryAdd() - .TryAdd() - .TryAddCoreServices(); - } -} diff --git a/src/EFCore.PG/Diagnostics/Internal/NpgsqlLoggingDefinitions.cs b/src/EFCore.PG/Diagnostics/Internal/NpgsqlLoggingDefinitions.cs deleted file mode 100644 index 20cd98a400..0000000000 --- a/src/EFCore.PG/Diagnostics/Internal/NpgsqlLoggingDefinitions.cs +++ /dev/null @@ -1,146 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.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 NpgsqlLoggingDefinitions : RelationalLoggingDefinitions -{ - /// - /// 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 EventDefinitionBase? LogFoundDefaultSchema; - - /// - /// 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 EventDefinitionBase? LogFoundColumn; - - /// - /// 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 EventDefinitionBase? LogFoundForeignKey; - - /// - /// 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 EventDefinitionBase? LogFoundCollation; - - /// - /// 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 EventDefinitionBase? LogPrincipalTableNotInSelectionSet; - - /// - /// 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 EventDefinitionBase? LogMissingSchema; - - /// - /// 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 EventDefinitionBase? LogMissingTable; - - /// - /// 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 EventDefinitionBase? LogFoundSequence; - - /// - /// 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 EventDefinitionBase? LogFoundTable; - - /// - /// 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 EventDefinitionBase? LogFoundIndex; - - /// - /// 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 EventDefinitionBase? LogFoundPrimaryKey; - - /// - /// 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 EventDefinitionBase? LogFoundUniqueConstraint; - - /// - /// 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 EventDefinitionBase? LogPrincipalColumnNotFound; - - /// - /// 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 EventDefinitionBase? LogEnumColumnSkipped; - - /// - /// 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 EventDefinitionBase? LogExpressionIndexSkipped; - - /// - /// 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 EventDefinitionBase? LogUnsupportedColumnConstraintSkipped; - - /// - /// 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 EventDefinitionBase? LogUnsupportedColumnIndexSkipped; -} diff --git a/src/EFCore.PG/Diagnostics/NpgsqlEfEventId.cs b/src/EFCore.PG/Diagnostics/NpgsqlEfEventId.cs deleted file mode 100644 index 328463e5c2..0000000000 --- a/src/EFCore.PG/Diagnostics/NpgsqlEfEventId.cs +++ /dev/null @@ -1,227 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore.Diagnostics; - -/// -/// -/// Event IDs for PostgreSQL/Npgsql events that correspond to messages logged to an -/// and events sent to a . -/// -/// -/// These IDs are also used with to configure the -/// behavior of warnings. -/// -/// -public static class NpgsqlEfEventId -{ - // Warning: These values must not change between releases. - // Only add new values to the end of sections, never in the middle. - // Try to use {Noun}{Verb} naming and be consistent with existing names. - private enum Id - { - // Model validation events - // Scaffolding events - ColumnFound = CoreEventId.ProviderDesignBaseId, - - //ColumnNotNamedWarning, - //ColumnSkipped, - //ForeignKeyColumnMissingWarning, - //ForeignKeyColumnNotNamedWarning, - //ForeignKeyColumnsNotMappedWarning, - //ForeignKeyNotNamedWarning, - ForeignKeyReferencesMissingPrincipalTableWarning, - - //IndexColumnFound, - //IndexColumnNotNamedWarning, - //IndexColumnSkipped, - //IndexColumnsNotMappedWarning, - //IndexNotNamedWarning, - //IndexTableMissingWarning, - MissingSchemaWarning, - MissingTableWarning, - SequenceFound, - - //SequenceNotNamedWarning, - TableFound, - - //TableSkipped, - //ForeignKeyTableMissingWarning, - PrimaryKeyFound, - UniqueConstraintFound, - IndexFound, - ForeignKeyFound, - ForeignKeyPrincipalColumnMissingWarning, - EnumColumnSkippedWarning, - ExpressionIndexSkippedWarning, - UnsupportedColumnIndexSkippedWarning, - UnsupportedConstraintIndexSkippedWarning, - CollationFound - } - - private static readonly string ScaffoldingPrefix = DbLoggerCategory.Scaffolding.Name + "."; - - private static EventId MakeScaffoldingId(Id id) - => new((int)id, ScaffoldingPrefix + id); - - /// - /// - /// A column was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId ColumnFound = MakeScaffoldingId(Id.ColumnFound); - - /// - /// - /// The database is missing a schema. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId MissingSchemaWarning = MakeScaffoldingId(Id.MissingSchemaWarning); - - /// - /// - /// A collation was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId CollationFound = MakeScaffoldingId(Id.CollationFound); - - /// - /// - /// The database is missing a table. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId MissingTableWarning = MakeScaffoldingId(Id.MissingTableWarning); - - /// - /// - /// A foreign key references a missing table at the principal end. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId ForeignKeyReferencesMissingPrincipalTableWarning = - MakeScaffoldingId(Id.ForeignKeyReferencesMissingPrincipalTableWarning); - - /// - /// - /// A table was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId TableFound = MakeScaffoldingId(Id.TableFound); - - /// - /// - /// A sequence was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId SequenceFound = MakeScaffoldingId(Id.SequenceFound); - - /// - /// - /// A primary key was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId PrimaryKeyFound = MakeScaffoldingId(Id.PrimaryKeyFound); - - /// - /// - /// A unique constraint was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId UniqueConstraintFound = MakeScaffoldingId(Id.UniqueConstraintFound); - - /// - /// - /// An index was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId IndexFound = MakeScaffoldingId(Id.IndexFound); - - /// - /// - /// A foreign key was found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId ForeignKeyFound = MakeScaffoldingId(Id.ForeignKeyFound); - - /// - /// - /// A principal column referenced by a foreign key was not found. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId ForeignKeyPrincipalColumnMissingWarning = MakeScaffoldingId(Id.ForeignKeyPrincipalColumnMissingWarning); - - /// - /// - /// Enum column cannot be scaffolded, define a CLR enum type and add the property manually. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId EnumColumnSkippedWarning = MakeScaffoldingId(Id.EnumColumnSkippedWarning); - - /// - /// - /// Expression index cannot be scaffolded, expression indices aren't supported and must be added via raw SQL in migrations. - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId ExpressionIndexSkippedWarning = MakeScaffoldingId(Id.ExpressionIndexSkippedWarning); - - /// - /// - /// Index '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId UnsupportedColumnIndexSkippedWarning = MakeScaffoldingId(Id.UnsupportedColumnIndexSkippedWarning); - - /// - /// - /// Constraint '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). - /// - /// - /// This event is in the category. - /// - /// - public static readonly EventId UnsupportedColumnConstraintSkippedWarning = - MakeScaffoldingId(Id.UnsupportedConstraintIndexSkippedWarning); -} diff --git a/src/EFCore.PG/EFCore.PG.csproj b/src/EFCore.PG/EFCore.PG.csproj deleted file mode 100644 index f6dd61b6cb..0000000000 --- a/src/EFCore.PG/EFCore.PG.csproj +++ /dev/null @@ -1,52 +0,0 @@ - - - - Npgsql.EntityFrameworkCore.PostgreSQL - Npgsql.EntityFrameworkCore.PostgreSQL - - Shay Rojansky;Austin Drenski;Yoh Deadfall; - PostgreSQL/Npgsql provider for Entity Framework Core. - npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql - README.md - EF9100 - - - - - - - - - - - - - - - - - - TextTemplatingFileGenerator - NpgsqlStrings.Designer.cs - - - - TextTemplatingFileGenerator - - - - True - True - NpgsqlStrings.Designer.tt - - - - Npgsql.EntityFrameworkCore.PostgreSQL.Internal - - - - - - - - diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs deleted file mode 100644 index 52d134bf49..0000000000 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlEntityTypeBuilderExtensions.cs +++ /dev/null @@ -1,297 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Npgsql-specific extension methods for . -/// -/// -/// See Modeling entity types and relationships. -/// -public static class NpgsqlEntityTypeBuilderExtensions -{ - #region Generated tsvector column - - // Note: actual configuration for generated TsVector properties is on the property - - /// - /// Configures a property on this entity to be a full-text search tsvector column over other given properties. - /// - /// The builder for the entity being configured. - /// - /// A lambda expression representing the property to be configured as a tsvector column - /// (blog => blog.Url). - /// - /// - /// - /// 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. - /// - /// - /// - /// - /// A lambda expression representing the property(s) to be included in the tsvector column - /// (blog => blog.Url). - /// - /// - /// If multiple properties are to be included then specify an anonymous type including the - /// properties (post => new { post.Title, post.BlogId }). - /// - /// - /// A builder to further configure the property. - public static EntityTypeBuilder HasGeneratedTsVectorColumn( - this EntityTypeBuilder entityTypeBuilder, - Expression> tsVectorPropertyExpression, - string config, - Expression> includeExpression) - where TEntity : class - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - Check.NotNull(tsVectorPropertyExpression, nameof(tsVectorPropertyExpression)); - Check.NotNull(config, nameof(config)); - Check.NotNull(includeExpression, nameof(includeExpression)); - - entityTypeBuilder.Property(tsVectorPropertyExpression).IsGeneratedTsVectorColumn( - config, - includeExpression.GetPropertyAccessList().Select(EntityFrameworkMemberInfoExtensions.GetSimpleMemberName).ToArray()); - - return entityTypeBuilder; - } - - #endregion Generated tsvector column - - #region Storage parameters - - /// - /// Sets a PostgreSQL storage parameter on the table created for this entity. - /// - /// - /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS - /// - /// The builder for the entity type being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// The same builder instance so that multiple calls can be chained. - public static EntityTypeBuilder HasStorageParameter( - this EntityTypeBuilder entityTypeBuilder, - string parameterName, - object? parameterValue) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - entityTypeBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); - - return entityTypeBuilder; - } - - /// - /// Sets a PostgreSQL storage parameter on the table created for this entity. - /// - /// - /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS - /// - /// The builder for the entity type being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// The same builder instance so that multiple calls can be chained. - public static EntityTypeBuilder HasStorageParameter( - this EntityTypeBuilder entityTypeBuilder, - string parameterName, - object? parameterValue) - where TEntity : class - => (EntityTypeBuilder)HasStorageParameter((EntityTypeBuilder)entityTypeBuilder, parameterName, parameterValue); - - /// - /// Sets a PostgreSQL storage parameter on the table created for this entity. - /// - /// - /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS - /// - /// The builder for the entity type being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// Indicates whether the configuration was specified using a data annotation. - /// The same builder instance so that multiple calls can be chained. - public static IConventionEntityTypeBuilder? HasStorageParameter( - this IConventionEntityTypeBuilder entityTypeBuilder, - string parameterName, - object? parameterValue, - bool fromDataAnnotation = false) - { - if (entityTypeBuilder.CanSetStorageParameter(parameterName, parameterValue, fromDataAnnotation)) - { - entityTypeBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); - - return entityTypeBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the PostgreSQL storage parameter is set on the table created for this entity. - /// - /// - /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS - /// - /// The builder for the entity type being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the mapped table can be configured as with the storage parameter. - public static bool CanSetStorageParameter( - this IConventionEntityTypeBuilder entityTypeBuilder, - string parameterName, - object? parameterValue, - bool fromDataAnnotation = false) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - return entityTypeBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); - } - - #endregion Storage parameters - - #region Unlogged Table - - /// - /// Configures the entity to use an unlogged table when targeting Npgsql. - /// - /// The builder for the entity type being configured. - /// True to configure the entity to use an unlogged table; otherwise, false. - /// - /// The same builder instance so that multiple calls can be chained. - /// - /// - /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED - /// - public static EntityTypeBuilder IsUnlogged( - this EntityTypeBuilder entityTypeBuilder, - bool unlogged = true) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - entityTypeBuilder.Metadata.SetIsUnlogged(unlogged); - - return entityTypeBuilder; - } - - /// - /// Configures the mapped table to use an unlogged table when targeting Npgsql. - /// - /// The builder for the entity type being configured. - /// True to configure the entity to use an unlogged table; otherwise, false. - /// - /// The same builder instance so that multiple calls can be chained. - /// - /// - /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED - /// - public static EntityTypeBuilder IsUnlogged( - this EntityTypeBuilder entityTypeBuilder, - bool unlogged = true) - where TEntity : class - => (EntityTypeBuilder)IsUnlogged((EntityTypeBuilder)entityTypeBuilder, unlogged); - - /// - /// Configures the mapped table to use an unlogged table when targeting Npgsql. - /// - /// The builder for the entity type being configured. - /// True to configure the entity to use an unlogged table; otherwise, false. - /// Indicates whether the configuration was specified using a data annotation. - /// - /// The same builder instance so that multiple calls can be chained. - /// - /// - /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED - /// - public static IConventionEntityTypeBuilder? IsUnlogged( - this IConventionEntityTypeBuilder entityTypeBuilder, - bool unlogged = true, - bool fromDataAnnotation = false) - { - if (entityTypeBuilder.CanSetIsUnlogged(unlogged, fromDataAnnotation)) - { - entityTypeBuilder.Metadata.SetIsUnlogged(unlogged, fromDataAnnotation); - - return entityTypeBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the mapped table can be configured to use an unlogged table when targeting Npgsql. - /// - /// The builder for the entity type being configured. - /// True to configure the entity to use an unlogged table; otherwise, false. - /// Indicates whether the configuration was specified using a data annotation. - /// - /// The same builder instance so that multiple calls can be chained. - /// - /// - /// See: https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED - /// - public static bool CanSetIsUnlogged( - this IConventionEntityTypeBuilder entityTypeBuilder, - bool unlogged = true, - bool fromDataAnnotation = false) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - return entityTypeBuilder.CanSetAnnotation(NpgsqlAnnotationNames.UnloggedTable, unlogged, fromDataAnnotation); - } - - #endregion - - #region CockroachDB Interleave-in-parent - - /// - /// Specifies that the CockroachDB-specific "interleave in parent" feature should be used. - /// - public static EntityTypeBuilder UseCockroachDbInterleaveInParent( - this EntityTypeBuilder entityTypeBuilder, - Type parentTableType, - List interleavePrefix) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - Check.NotNull(parentTableType, nameof(parentTableType)); - Check.NotNull(interleavePrefix, nameof(interleavePrefix)); - - var parentEntity = entityTypeBuilder.Metadata.Model.FindEntityType(parentTableType); - if (parentEntity is null) - { - throw new ArgumentException($"Entity not found in model for type: {parentEntity}", nameof(parentTableType)); - } - - if (StoreObjectIdentifier.Create(parentEntity, StoreObjectType.Table) is not { } tableIdentifier) - { - throw new ArgumentException($"Entity {parentEntity.DisplayName()} is not mapped to a database table"); - } - - var interleaveInParent = entityTypeBuilder.Metadata.GetCockroachDbInterleaveInParent(); - interleaveInParent.ParentTableSchema = tableIdentifier.Schema; - interleaveInParent.ParentTableName = tableIdentifier.Name; - interleaveInParent.InterleavePrefix = interleavePrefix; - - return entityTypeBuilder; - } - - /// - /// Specifies that the CockroachDB-specific "interleave in parent" feature should be used. - /// - public static EntityTypeBuilder UseCockroachDbInterleaveInParent( - this EntityTypeBuilder entityTypeBuilder, - Type parentTableType, - List interleavePrefix) - where TEntity : class - => (EntityTypeBuilder)UseCockroachDbInterleaveInParent( - (EntityTypeBuilder)entityTypeBuilder, parentTableType, interleavePrefix); - - #endregion CockroachDB Interleave-in-parent -} diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs deleted file mode 100644 index 741bff72a3..0000000000 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlIndexBuilderExtensions.cs +++ /dev/null @@ -1,850 +0,0 @@ -using System.Collections; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Npgsql specific extension methods for . -/// -public static class NpgsqlIndexBuilderExtensions -{ - #region Method - - /// - /// 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. - /// A builder to further configure the index. - public static IndexBuilder HasMethod( - this IndexBuilder indexBuilder, - string? method) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(method, nameof(method)); - - indexBuilder.Metadata.SetMethod(method); - - return indexBuilder; - } - - /// - /// 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. - /// A builder to further configure the index. - public static IndexBuilder HasMethod( - this IndexBuilder indexBuilder, - string? method) - => (IndexBuilder)HasMethod((IndexBuilder)indexBuilder, method); - - /// - /// 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. - /// A builder to further configure the index. - public static IConventionIndexBuilder? HasMethod( - this IConventionIndexBuilder indexBuilder, - string? method, - bool fromDataAnnotation = false) - { - if (indexBuilder.CanSetMethod(method, fromDataAnnotation)) - { - indexBuilder.Metadata.SetMethod(method, fromDataAnnotation); - - return indexBuilder; - } - - return null; - } - - /// - /// 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 - public static bool CanSetMethod( - this IConventionIndexBuilder indexBuilder, - string? method, - bool fromDataAnnotation = false) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.IndexMethod, method, fromDataAnnotation); - } - - #endregion Method - - #region Operators - - /// - /// The PostgreSQL index operators to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-opclass.html - /// - /// The builder for the index being configured. - /// The operators to use for each column. - /// A builder to further configure the index. - public static IndexBuilder HasOperators( - this IndexBuilder indexBuilder, - params string[]? operators) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(operators, nameof(operators)); - - indexBuilder.Metadata.SetOperators(operators); - - return indexBuilder; - } - - /// - /// The PostgreSQL index operators to be used. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-opclass.html - /// - /// The builder for the index being configured. - /// The operators to use for each column. - /// A builder to further configure the index. - public static IndexBuilder HasOperators( - this IndexBuilder indexBuilder, - params string[]? operators) - => (IndexBuilder)HasOperators((IndexBuilder)indexBuilder, operators); - - /// - /// The PostgreSQL index operators to be used. - /// - /// - /// 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. - /// A builder to further configure the index. - public static IConventionIndexBuilder? HasOperators( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? operators, - bool fromDataAnnotation) - { - if (indexBuilder.CanSetOperators(operators, fromDataAnnotation)) - { - indexBuilder.Metadata.SetOperators(operators, fromDataAnnotation); - - return indexBuilder; - } - - return null; - } - - /// - /// 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. - public static bool CanSetOperators( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? operators, - bool fromDataAnnotation) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.IndexOperators, operators, fromDataAnnotation); - } - - #endregion Operators - - #region IsTsVectorExpressionIndex - - /// - /// Configures this index to be 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. - /// - /// - /// A builder to further configure the index. - public static IndexBuilder IsTsVectorExpressionIndex( - this IndexBuilder indexBuilder, - string config) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NotNull(config, nameof(config)); - - indexBuilder.Metadata.SetTsVectorConfig(config); - return indexBuilder; - } - - /// - /// Configures this index to be 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. - /// - /// - /// A builder to further configure the index. - public static IndexBuilder IsTsVectorExpressionIndex( - this IndexBuilder indexBuilder, - string config) - => (IndexBuilder)IsTsVectorExpressionIndex((IndexBuilder)indexBuilder, config); - - /// - /// Configures this index to be 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. - /// - /// - /// - /// The same builder instance if the configuration was applied, - /// null otherwise. - /// - public static IConventionIndexBuilder? IsTsVectorExpressionIndex( - this IConventionIndexBuilder indexBuilder, - string? config) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - if (indexBuilder.CanSetIsTsVectorExpressionIndex(config)) - { - indexBuilder.Metadata.SetTsVectorConfig(config); - - return indexBuilder; - } - - return null; - } - - /// - /// 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. - public static bool CanSetIsTsVectorExpressionIndex( - this IConventionIndexBuilder indexBuilder, - string? config, - bool fromDataAnnotation = false) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.TsVectorConfig, config, fromDataAnnotation); - } - - #endregion IsTsVectorExpressionIndex - - #region Collation - - /// - /// 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. - public static IndexBuilder UseCollation( - this IndexBuilder indexBuilder, - params string[]? values) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(values, nameof(values)); - - indexBuilder.Metadata.SetCollation(values); - - return indexBuilder; - } - - /// - /// 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. - public static IndexBuilder UseCollation( - this IndexBuilder indexBuilder, - params string[]? values) - => (IndexBuilder)UseCollation((IndexBuilder)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. - public static IConventionIndexBuilder? UseCollation( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - { - if (indexBuilder.CanSetCollation(values, fromDataAnnotation)) - { - indexBuilder.Metadata.SetCollation(values, fromDataAnnotation); - - return indexBuilder; - } - - return null; - } - - /// - /// 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. - public static bool CanSetCollation( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(RelationalAnnotationNames.Collation, values, fromDataAnnotation); - } - - #endregion Collation - - #region Null sort order - - /// - /// The PostgreSQL index NULL 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. - public static IndexBuilder HasNullSortOrder( - this IndexBuilder indexBuilder, - params NullSortOrder[]? values) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(values, nameof(values)); - - if (!SortOrderHelper.IsDefaultNullSortOrder(values, indexBuilder.Metadata.IsDescending)) - { - indexBuilder.Metadata.SetNullSortOrder(values); - } - - return indexBuilder; - } - - /// - /// The PostgreSQL index NULL 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. - public static IndexBuilder HasNullSortOrder( - this IndexBuilder indexBuilder, - params NullSortOrder[]? values) - => (IndexBuilder)HasNullSortOrder((IndexBuilder)indexBuilder, values); - - /// - /// The PostgreSQL index NULL 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. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - public static IConventionIndexBuilder? HasNullSortOrder( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - { - if (indexBuilder.CanSetNullSortOrder(values, fromDataAnnotation)) - { - if (!SortOrderHelper.IsDefaultNullSortOrder(values, indexBuilder.Metadata.IsDescending)) - { - indexBuilder.Metadata.SetNullSortOrder(values, fromDataAnnotation); - } - - return indexBuilder; - } - - return null; - } - - /// - /// 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. - public static bool CanSetNullSortOrder( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? values, - bool fromDataAnnotation) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.IndexNullSortOrder, values, fromDataAnnotation); - } - - #endregion Null sort order - - #region Include - - /// - /// Adds an INCLUDE clause to the index definition with the specified property names. - /// This clause specifies a list of columns which will be included as a non-key part in the index. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// An array of property names to be used in INCLUDE clause. - /// A builder to further configure the index. - public static IndexBuilder IncludeProperties( - this IndexBuilder indexBuilder, - params string[] propertyNames) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(propertyNames, nameof(propertyNames)); - - indexBuilder.Metadata.SetIncludeProperties(propertyNames); - - return indexBuilder; - } - - /// - /// Adds an INCLUDE clause to the index definition with the specified property names. - /// This clause specifies a list of columns which will be included as a non-key part in the index. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// An array of property names to be used in INCLUDE clause. - /// A builder to further configure the index. - public static IndexBuilder IncludeProperties( - this IndexBuilder indexBuilder, - params string[] propertyNames) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(propertyNames, nameof(propertyNames)); - - indexBuilder.Metadata.SetIncludeProperties(propertyNames); - - return indexBuilder; - } - - /// - /// Adds an INCLUDE clause to the index definition with property names from the specified expression. - /// This clause specifies a list of columns which will be included as a non-key part in the index. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// - /// - /// A lambda expression representing the property(s) to be included in the INCLUDE clause - /// (blog => blog.Url). - /// - /// - /// If multiple properties are to be included then specify an anonymous type including the - /// properties (post => new { post.Title, post.BlogId }). - /// - /// - /// A builder to further configure the index. - public static IndexBuilder IncludeProperties( - this IndexBuilder indexBuilder, - Expression> includeExpression) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NotNull(includeExpression, nameof(includeExpression)); - - indexBuilder.IncludeProperties(includeExpression.GetPropertyAccessList().Select(x => x.Name).ToArray()); - - return indexBuilder; - } - - /// - /// Adds an INCLUDE clause to the index definition with the specified property names. - /// This clause specifies a list of columns which will be included as a non-key part in the index. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// 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. - /// A builder to further configure the index. - public static IConventionIndexBuilder? IncludeProperties( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList propertyNames, - bool fromDataAnnotation = false) - { - if (indexBuilder.CanSetIncludeProperties(propertyNames, fromDataAnnotation)) - { - indexBuilder.Metadata.SetIncludeProperties(propertyNames, fromDataAnnotation); - - return indexBuilder; - } - - return null; - } - - /// - /// 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. - public static bool CanSetIncludeProperties( - this IConventionIndexBuilder indexBuilder, - IReadOnlyList? propertyNames, - bool fromDataAnnotation = false) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(indexBuilder.Metadata.GetIncludePropertiesConfigurationSource()) - || StructuralComparisons.StructuralEqualityComparer.Equals( - propertyNames, indexBuilder.Metadata.GetIncludeProperties()); - } - - #endregion Include - - #region Created concurrently - - /// - /// When this option is used, PostgreSQL will build the index without taking any locks that prevent concurrent inserts, - /// updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY - /// - /// The builder for the index being configured. - /// A value indicating whether the index is created with the "concurrently" option. - /// A builder to further configure the index. - public static IndexBuilder IsCreatedConcurrently(this IndexBuilder indexBuilder, bool createdConcurrently = true) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - indexBuilder.Metadata.SetIsCreatedConcurrently(createdConcurrently); - - return indexBuilder; - } - - /// - /// When this option is used, PostgreSQL will build the index without taking any locks that prevent concurrent inserts, - /// updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY - /// - /// The builder for the index being configured. - /// A value indicating whether the index is created with the "concurrently" option. - /// A builder to further configure the index. - public static IndexBuilder IsCreatedConcurrently( - this IndexBuilder indexBuilder, - bool createdConcurrently = true) - => (IndexBuilder)IsCreatedConcurrently((IndexBuilder)indexBuilder, createdConcurrently); - - /// - /// When this option is used, PostgreSQL will build the index without taking any locks that prevent concurrent inserts, - /// updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY - /// - /// The builder for the index being configured. - /// A value indicating whether the index is created with the "concurrently" option. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - public static IConventionIndexBuilder? IsCreatedConcurrently( - this IConventionIndexBuilder indexBuilder, - bool? createdConcurrently, - bool fromDataAnnotation = false) - { - if (indexBuilder.CanSetIsCreatedConcurrently(createdConcurrently, fromDataAnnotation)) - { - indexBuilder.Metadata.SetIsCreatedConcurrently(createdConcurrently); - - return indexBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether concurrent creation for the index can be set. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY - /// - /// The builder for the index being configured. - /// A value indicating whether the index is created with the "concurrently" option. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - public static bool CanSetIsCreatedConcurrently( - this IConventionIndexBuilder indexBuilder, - bool? createdConcurrently, - bool fromDataAnnotation = false) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.CreatedConcurrently, createdConcurrently, fromDataAnnotation); - } - - #endregion Created concurrently - - #region NULLS distinct - - /// - /// Specifies whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// Whether nulls should be considered distinct. - /// A builder to further configure the index. - public static IndexBuilder AreNullsDistinct( - this IndexBuilder indexBuilder, - bool nullsDistinct = true) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - indexBuilder.Metadata.SetAreNullsDistinct(nullsDistinct); - - return indexBuilder; - } - - /// - /// Specifies whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// Whether nulls should be considered distinct. - /// A builder to further configure the index. - public static IndexBuilder AreNullsDistinct( - this IndexBuilder indexBuilder, - bool nullsDistinct = true) - => (IndexBuilder)AreNullsDistinct((IndexBuilder)indexBuilder, nullsDistinct); - - /// - /// Specifies whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// Whether nulls should be considered distinct. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the index. - public static IConventionIndexBuilder? AreNullsDistinct( - this IConventionIndexBuilder indexBuilder, - bool nullsDistinct = true, - bool fromDataAnnotation = false) - { - if (indexBuilder.CanSetAreNullsDistinct(nullsDistinct, fromDataAnnotation)) - { - indexBuilder.Metadata.SetAreNullsDistinct(nullsDistinct, fromDataAnnotation); - - return indexBuilder; - } - - return null; - } - - /// - /// Specifies whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// https://www.postgresql.org/docs/current/sql-createindex.html - /// - /// The builder for the index being configured. - /// Whether nulls should be considered distinct. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured with the method - public static bool CanSetAreNullsDistinct( - this IConventionIndexBuilder indexBuilder, - bool nullsDistinct = true, - bool fromDataAnnotation = false) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation(NpgsqlAnnotationNames.NullsDistinct, nullsDistinct, fromDataAnnotation); - } - - #endregion NULLS distinct - - #region Storage parameters - - /// - /// Sets a PostgreSQL storage parameter on the index. - /// - /// - /// See https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS - /// - /// The builder for the index being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// The same builder instance so that multiple calls can be chained. - public static IndexBuilder HasStorageParameter( - this IndexBuilder indexBuilder, - string parameterName, - object? parameterValue) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - indexBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); - - return indexBuilder; - } - - /// - /// Sets a PostgreSQL storage parameter on the index. - /// - /// - /// See https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS - /// - /// The builder for the index being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// The same builder instance so that multiple calls can be chained. - public static IndexBuilder HasStorageParameter( - this IndexBuilder indexBuilder, - string parameterName, - object? parameterValue) - where TEntity : class - => (IndexBuilder)HasStorageParameter((IndexBuilder)indexBuilder, parameterName, parameterValue); - - /// - /// Sets a PostgreSQL storage parameter on the index. - /// - /// - /// See https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS - /// - /// The builder for the index being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured with the method - public static IConventionIndexBuilder? HasStorageParameter( - this IConventionIndexBuilder indexBuilder, - string parameterName, - object? parameterValue, - bool fromDataAnnotation = false) - { - if (indexBuilder.CanSetStorageParameter(parameterName, parameterValue, fromDataAnnotation)) - { - indexBuilder.Metadata.SetStorageParameter(parameterName, parameterValue); - - return indexBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the PostgreSQL storage parameter is set on the table created for this entity. - /// - /// - /// See https://www.postgresql.org/docs/current/static/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS - /// - /// The builder for the index being configured. - /// The name of the storage parameter. - /// The value of the storage parameter. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured as with the storage parameter. - public static bool CanSetStorageParameter( - this IConventionIndexBuilder indexBuilder, - string parameterName, - object? parameterValue, - bool fromDataAnnotation = false) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - - return indexBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); - } - - #endregion Storage parameters -} diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs deleted file mode 100644 index 6650eb824e..0000000000 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs +++ /dev/null @@ -1,787 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.NameTranslation; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Npgsql-specific extension methods for . -/// -public static class NpgsqlModelBuilderExtensions -{ - #region HiLo - - /// - /// Configures the model to use a sequence-based hi-lo pattern to generate values for properties - /// marked as , when targeting PostgreSQL. - /// - /// The model builder. - /// The name of the sequence. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder UseHiLo(this ModelBuilder modelBuilder, string? name = null, string? schema = null) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NullButNotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - var model = modelBuilder.Model; - - name ??= NpgsqlModelExtensions.DefaultHiLoSequenceName; - - if (model.FindSequence(name, schema) is null) - { - modelBuilder.HasSequence(name, schema).IncrementsBy(10); - } - - model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - model.SetHiLoSequenceName(name); - model.SetHiLoSequenceSchema(schema); - model.SetSequenceNameSuffix(null); - model.SetSequenceSchema(null); - - return modelBuilder; - } - - /// - /// Configures the database sequence used for the hi-lo pattern to generate values for key properties - /// marked as , when targeting PostgreSQL. - /// - /// The model builder. - /// The name of the sequence. - /// The schema of the sequence. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the sequence. - public static IConventionSequenceBuilder? HasHiLoSequence( - this IConventionModelBuilder modelBuilder, - string? name, - string? schema, - bool fromDataAnnotation = false) - { - if (!modelBuilder.CanSetHiLoSequence(name, schema)) - { - return null; - } - - modelBuilder.Metadata.SetHiLoSequenceName(name, fromDataAnnotation); - modelBuilder.Metadata.SetHiLoSequenceSchema(schema, fromDataAnnotation); - - return name is null ? null : modelBuilder.HasSequence(name, schema, fromDataAnnotation); - } - - /// - /// Returns a value indicating whether the given name and schema can be set for the hi-lo sequence. - /// - /// The model builder. - /// The name of the sequence. - /// The schema of the sequence. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the given name and schema can be set for the hi-lo sequence. - public static bool CanSetHiLoSequence( - this IConventionModelBuilder modelBuilder, - string? name, - string? schema, - bool fromDataAnnotation = false) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NullButNotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - return modelBuilder.CanSetAnnotation(NpgsqlAnnotationNames.HiLoSequenceName, name, fromDataAnnotation) - && modelBuilder.CanSetAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema, schema, fromDataAnnotation); - } - - #endregion HiLo - - #region Serial - - /// - /// - /// Configures the model to use the PostgreSQL SERIAL feature to generate values for properties - /// marked as , when targeting PostgreSQL. - /// - /// - /// This option should be considered deprecated starting with PostgreSQL 10, consider using instead. - /// - /// - /// The model builder. - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder UseSerialColumns( - this ModelBuilder modelBuilder) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - - var property = modelBuilder.Model; - - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SerialColumn); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); - - return modelBuilder; - } - - #endregion Serial - - #region Identity - - /// - /// - /// Configures the model to use the PostgreSQL IDENTITY feature to generate values for properties - /// marked as , when targeting PostgreSQL. Values for these - /// columns will always be generated as identity, and the application will not be able to override - /// this behavior by providing a value. - /// - /// Available only starting PostgreSQL 10. - /// - /// The model builder. - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder UseIdentityAlwaysColumns(this ModelBuilder modelBuilder) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - - var model = modelBuilder.Model; - - model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn); - model.SetSequenceNameSuffix(null); - model.SetSequenceSchema(null); - model.SetHiLoSequenceName(null); - model.SetHiLoSequenceSchema(null); - - return modelBuilder; - } - - /// - /// - /// Configures the model to use the PostgreSQL IDENTITY feature to generate values for properties - /// marked as , when targeting PostgreSQL. Values for these - /// columns will be generated as identity by default, but the application will be able to override - /// this behavior by providing a value. - /// - /// - /// This is the default behavior when targeting PostgreSQL. Available only starting PostgreSQL 10. - /// - /// - /// The model builder. - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder UseIdentityByDefaultColumns(this ModelBuilder modelBuilder) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - - var model = modelBuilder.Model; - - model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - model.SetSequenceNameSuffix(null); - model.SetSequenceSchema(null); - model.SetHiLoSequenceName(null); - model.SetHiLoSequenceSchema(null); - - return modelBuilder; - } - - /// - /// - /// Configures the model to use the PostgreSQL IDENTITY feature to generate values for properties - /// marked as , when targeting PostgreSQL. Values for these - /// columns will be generated as identity by default, but the application will be able to override - /// this behavior by providing a value. - /// - /// - /// This is the default behavior when targeting PostgreSQL. Available only starting PostgreSQL 10. - /// - /// - /// The model builder. - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder UseIdentityColumns(this ModelBuilder modelBuilder) - => modelBuilder.UseIdentityByDefaultColumns(); - - /// - /// Configures the value generation strategy for the key property, when targeting PostgreSQL. - /// - /// The builder for the property being configured. - /// The value generation strategy. - /// Indicates whether the configuration was specified using a data annotation. - /// - /// The same builder instance if the configuration was applied, null otherwise. - /// - public static IConventionModelBuilder? HasValueGenerationStrategy( - this IConventionModelBuilder modelBuilder, - NpgsqlValueGenerationStrategy? valueGenerationStrategy, - bool fromDataAnnotation = false) - { - if (modelBuilder.CanSetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation)) - { - modelBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); - - if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.SequenceHiLo) - { - modelBuilder.HasHiLoSequence(null, null, fromDataAnnotation); - } - - if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.Sequence) - { - if (modelBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix, null) - && modelBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceSchema, null)) - { - modelBuilder.Metadata.SetSequenceNameSuffix(null, fromDataAnnotation); - modelBuilder.Metadata.SetSequenceSchema(null, fromDataAnnotation); - } - } - - return modelBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the given value can be set as the default value generation strategy. - /// - /// The model builder. - /// The value generation strategy. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the given value can be set as the default value generation strategy. - public static bool CanSetValueGenerationStrategy( - this IConventionModelBuilder modelBuilder, - NpgsqlValueGenerationStrategy? valueGenerationStrategy, - bool fromDataAnnotation = false) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - - return modelBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); - } - - #endregion Identity - - #region Sequence - - /// - /// Configures the model to use a sequence per hierarchy to generate values for key properties marked as - /// , when targeting PostgreSQL. - /// - /// The model builder. - /// The name that will suffix the table name for each sequence created automatically. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder UseKeySequences( - this ModelBuilder modelBuilder, - string? nameSuffix = null, - string? schema = null) - { - Check.NullButNotEmpty(nameSuffix, nameof(nameSuffix)); - Check.NullButNotEmpty(schema, nameof(schema)); - - var model = modelBuilder.Model; - - nameSuffix ??= NpgsqlModelExtensions.DefaultSequenceNameSuffix; - - model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.Sequence); - model.SetSequenceNameSuffix(nameSuffix); - model.SetSequenceSchema(schema); - model.SetHiLoSequenceName(null); - model.SetHiLoSequenceSchema(null); - - return modelBuilder; - } - - #endregion Sequence - - #region Extensions - - /// - /// Registers a PostgreSQL extension in the model. - /// - /// The model builder in which to define the extension. - /// The schema in which to create the extension. - /// The name of the extension to create. - /// The version of the extension. - /// The same builder instance so that multiple calls can be chained. - /// - /// See: https://www.postgresql.org/docs/current/external-extensions.html - /// - /// - /// - /// - public static ModelBuilder HasPostgresExtension( - this ModelBuilder modelBuilder, - string? schema, - string name, - string? version = null) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - modelBuilder.Model.GetOrAddPostgresExtension(schema, name, version); - - return modelBuilder; - } - - /// - /// Registers a PostgreSQL extension in the model. - /// - /// The model builder in which to define the extension. - /// The name of the extension to create. - /// The same builder instance so that multiple calls can be chained. - /// - /// See: https://www.postgresql.org/docs/current/external-extensions.html - /// - /// - /// - /// - public static ModelBuilder HasPostgresExtension( - this ModelBuilder modelBuilder, - string name) - => modelBuilder.HasPostgresExtension(null, name); - - /// - /// Registers a PostgreSQL extension in the model. - /// - /// The model builder in which to define the extension. - /// The schema in which to create the extension. - /// The name of the extension to create. - /// The version of the extension. - /// Indicates whether the configuration was specified using a data annotation. - /// The same builder instance so that multiple calls can be chained. - /// - /// See: https://www.postgresql.org/docs/current/external-extensions.html - /// - /// - /// - /// - public static IConventionModelBuilder? HasPostgresExtension( - this IConventionModelBuilder modelBuilder, - string? schema, - string name, - string? version = null, - bool fromDataAnnotation = false) - { - if (modelBuilder.CanSetPostgresExtension(schema, name, version, fromDataAnnotation)) - { - modelBuilder.Metadata.GetOrAddPostgresExtension(schema, name, version); - return modelBuilder; - } - - return null; - } - - /// - /// Registers a PostgreSQL extension in the model. - /// - /// The model builder in which to define the extension. - /// The name of the extension to create. - /// Indicates whether the configuration was specified using a data annotation. - /// The same builder instance so that multiple calls can be chained. - /// - /// See: https://www.postgresql.org/docs/current/external-extensions.html - /// - /// - /// - /// - public static IConventionModelBuilder? HasPostgresExtension( - this IConventionModelBuilder modelBuilder, - string name, - bool fromDataAnnotation = false) - => modelBuilder.HasPostgresExtension(schema: null, name, version: null, fromDataAnnotation); - - /// - /// 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. - /// The version of the extension. - /// 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 CanSetPostgresExtension( - this IConventionModelBuilder modelBuilder, - string? schema, - string name, - string? version = null, - bool fromDataAnnotation = false) - { - var annotationName = PostgresExtension.BuildAnnotationName(schema, name); - - return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name},{version}", fromDataAnnotation); - } - - #endregion - - #region Enums - - /// - /// 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 ModelBuilder HasPostgresEnum( - this ModelBuilder modelBuilder, - string? schema, - string name, - string[] labels) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NotNull(labels, nameof(labels)); - - modelBuilder.Model.GetOrAddPostgresEnum(schema, name, labels); - 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. - /// - /// The model builder 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 ModelBuilder HasPostgresEnum( - this ModelBuilder modelBuilder, - string name, - string[] labels) - => modelBuilder.HasPostgresEnum(null, name, labels); - - /// - /// 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 translator for name and label inference. - /// Defaults to . - /// - /// - /// - /// The updated . - /// - /// - /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html - /// - /// builder - public static ModelBuilder HasPostgresEnum( - this ModelBuilder modelBuilder, - string? schema = null, - string? name = null, - INpgsqlNameTranslator? nameTranslator = null) - where TEnum : struct, Enum - { -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; -#pragma warning restore CS0618 - - return modelBuilder.HasPostgresEnum( - schema, - name ?? GetTypePgName(nameTranslator), - GetMemberPgNames(nameTranslator)); - } - - #endregion - - #region Templates - - /// - /// Specifies the PostgreSQL database to use as a template when creating a new database for this model. - /// - public static ModelBuilder UseDatabaseTemplate(this ModelBuilder modelBuilder, string templateDatabaseName) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(templateDatabaseName, nameof(templateDatabaseName)); - - modelBuilder.Model.SetDatabaseTemplate(templateDatabaseName); - return modelBuilder; - } - - #endregion - - #region Ranges - - /// - /// Registers a user-defined range type in the model. - /// - /// The model builder on which to create the range type. - /// The schema in which to create the range type. - /// The name of the range type to be created. - /// The subtype (or element type) of the range - /// - /// An optional PostgreSQL function which converts range values to a canonical form. - /// - /// Used to specify a non-default operator class. - /// Used to specify a non-default collation in the range's order. - /// - /// An optional PostgreSQL function taking two values of the subtype type as argument, and return a double - /// precision value representing the difference between the two given values. - /// - /// - /// See https://www.postgresql.org/docs/current/static/rangetypes.html, - /// https://www.postgresql.org/docs/current/static/sql-createtype.html, - /// - public static ModelBuilder HasPostgresRange( - this ModelBuilder modelBuilder, - string? schema, - string name, - string subtype, - string? canonicalFunction = null, - string? subtypeOpClass = null, - string? collation = null, - string? subtypeDiff = null) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NotEmpty(subtype, nameof(subtype)); - - modelBuilder.Model.GetOrAddPostgresRange( - schema, - name, - subtype, - canonicalFunction, - subtypeOpClass, - collation, - subtypeDiff); - return modelBuilder; - } - - /// - /// Registers a user-defined range type in the model. - /// - /// The model builder on which to create the range type. - /// The name of the range type to be created. - /// The subtype (or element type) of the range - /// - /// See https://www.postgresql.org/docs/current/static/rangetypes.html, - /// https://www.postgresql.org/docs/current/static/sql-createtype.html, - /// - public static ModelBuilder HasPostgresRange( - this ModelBuilder modelBuilder, - string name, - string subtype) - => HasPostgresRange(modelBuilder, null, name, subtype); - - #endregion - - #region Tablespaces - - /// - /// Specifies the PostgreSQL tablespace in which to place the new database created for this model. - /// - public static ModelBuilder UseTablespace( - this ModelBuilder modelBuilder, - string tablespace) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(tablespace, nameof(tablespace)); - - modelBuilder.Model.SetTablespace(tablespace); - return modelBuilder; - } - - #endregion - - #region Collation management - - /// - /// Creates a new collation in the database. - /// - /// - /// See https://www.postgresql.org/docs/current/sql-createcollation.html. - /// - /// The model builder on which to create the collation. - /// The name of the collation to create. - /// Sets LC_COLLATE and LC_CTYPE at once. - /// - /// Specifies the provider to use for locale services associated with this collation. - /// The available choices depend on the operating system and build options. - /// - /// - /// Specifies whether the collation should use deterministic comparisons. - /// Defaults to true. - /// - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder HasCollation( - this ModelBuilder modelBuilder, - string name, - string locale, - string? provider = null, - bool? deterministic = null) - => modelBuilder.HasCollation(schema: null, name, locale, provider: provider, deterministic: deterministic); - - /// - /// Creates a new collation in the database. - /// - /// - /// See https://www.postgresql.org/docs/current/sql-createcollation.html. - /// - /// The model builder on which to create the collation. - /// The schema in which to create the collation, or null for the default schema. - /// The name of the collation to create. - /// Sets LC_COLLATE and LC_CTYPE at once. - /// - /// Specifies the provider to use for locale services associated with this collation. - /// The available choices depend on the operating system and build options. - /// - /// - /// Specifies whether the collation should use deterministic comparisons. - /// Defaults to true. - /// - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder HasCollation( - this ModelBuilder modelBuilder, - string? schema, - string name, - string locale, - string? provider = null, - bool? deterministic = null) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NotEmpty(locale, nameof(locale)); - - modelBuilder.Model.GetOrAddCollation( - schema, - name, - locale, - locale, - provider, - deterministic); - return modelBuilder; - } - - /// - /// Creates a new collation in the database. - /// - /// - /// See https://www.postgresql.org/docs/current/sql-createcollation.html. - /// - /// The model builder on which to create the collation. - /// The schema in which to create the collation, or null for the default schema. - /// The name of the collation to create. - /// Use the specified operating system locale for the LC_COLLATE locale category. - /// Use the specified operating system locale for the LC_CTYPE locale category. - /// - /// Specifies the provider to use for locale services associated with this collation. - /// The available choices depend on the operating system and build options. - /// - /// - /// Specifies whether the collation should use deterministic comparisons. - /// Defaults to true. - /// - /// The same builder instance so that multiple calls can be chained. - public static ModelBuilder HasCollation( - this ModelBuilder modelBuilder, - string? schema, - string name, - string lcCollate, - string lcCtype, - string? provider = null, - bool? deterministic = null) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NotEmpty(lcCollate, nameof(lcCollate)); - Check.NotEmpty(lcCtype, nameof(lcCtype)); - - modelBuilder.Model.GetOrAddCollation( - schema, - name, - lcCollate, - lcCtype, - provider, - deterministic); - return modelBuilder; - } - - #endregion Collation management - - #region Helpers - - // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeMapping/TypeMapperBase.cs#L132-L138 - private static string GetTypePgName(INpgsqlNameTranslator nameTranslator) - where TEnum : struct, Enum - => typeof(TEnum).GetCustomAttribute()?.PgName ?? nameTranslator.TranslateTypeName(typeof(TEnum).Name); - - // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeHandlers/EnumHandler.cs#L118-L129 - private static string[] GetMemberPgNames(INpgsqlNameTranslator nameTranslator) - where TEnum : struct, Enum - => typeof(TEnum) - .GetFields(BindingFlags.Static | BindingFlags.Public) - .Select(x => x.GetCustomAttribute()?.PgName ?? nameTranslator.TranslateMemberName(x.Name)) - .ToArray(); - - #endregion -} diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs deleted file mode 100644 index 2698800d38..0000000000 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlPropertyBuilderExtensions.cs +++ /dev/null @@ -1,914 +0,0 @@ -using System.Collections; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Npgsql specific extension methods for . -/// -public static class NpgsqlPropertyBuilderExtensions -{ - #region HiLo - - /// - /// Configures the property to use a sequence-based hi-lo pattern to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// - /// The builder for the property being configured. - /// The comment of the sequence. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseHiLo( - this PropertyBuilder propertyBuilder, - string? name = null, - string? schema = null) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - Check.NullButNotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - var property = propertyBuilder.Metadata; - - name ??= NpgsqlModelExtensions.DefaultHiLoSequenceName; - - var model = property.DeclaringType.Model; - - if (model.FindSequence(name, schema) is null) - { - model.AddSequence(name, schema).IncrementBy = 10; - } - - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - property.SetHiLoSequenceName(name); - property.SetHiLoSequenceSchema(schema); - property.SetSequenceName(null); - property.SetSequenceSchema(null); - property.RemoveIdentityOptions(); - - return propertyBuilder; - } - - /// - /// Configures the property to use a sequence-based hi-lo pattern to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// - /// The builder for the property being configured. - /// The comment of the sequence. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseHiLo( - this PropertyBuilder propertyBuilder, - string? name = null, - string? schema = null) - => (PropertyBuilder)UseHiLo((PropertyBuilder)propertyBuilder, name, schema); - - /// - /// Configures the database sequence used for the hi-lo pattern to generate values for the key property, - /// when targeting SQL Server. - /// - /// The builder for the property being configured. - /// The name of the sequence. - /// The schema of the sequence. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the sequence. - public static IConventionSequenceBuilder? HasHiLoSequence( - this IConventionPropertyBuilder propertyBuilder, - string? name, - string? schema, - bool fromDataAnnotation = false) - { - if (!propertyBuilder.CanSetHiLoSequence(name, schema)) - { - return null; - } - - propertyBuilder.Metadata.SetHiLoSequenceName(name, fromDataAnnotation); - propertyBuilder.Metadata.SetHiLoSequenceSchema(schema, fromDataAnnotation); - - return name is null - ? null - : propertyBuilder.Metadata.DeclaringType.Model.Builder.HasSequence(name, schema, fromDataAnnotation); - } - - /// - /// Returns a value indicating whether the given name and schema can be set for the hi-lo sequence. - /// - /// The builder for the property being configured. - /// The name of the sequence. - /// The schema of the sequence. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the given name and schema can be set for the hi-lo sequence. - public static bool CanSetHiLoSequence( - this IConventionPropertyBuilder propertyBuilder, - string? name, - string? schema, - bool fromDataAnnotation = false) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - Check.NullButNotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - return propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.HiLoSequenceName, name, fromDataAnnotation) - && propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema, schema, fromDataAnnotation); - } - - #endregion HiLo - - #region Sequence - - /// - /// Configures the key property to use a sequence-based key value generation pattern to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// - /// The builder for the property being configured. - /// The name of the sequence. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseSequence( - this PropertyBuilder propertyBuilder, - string? name = null, - string? schema = null) - { - Check.NullButNotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - var property = propertyBuilder.Metadata; - - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.Sequence); - property.SetSequenceName(name); - property.SetSequenceSchema(schema); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); - - return propertyBuilder; - } - - /// - /// Configures the key property to use a sequence-based key value generation pattern to generate values for new entities, - /// when targeting SQL Server. This method sets the property to be . - /// - /// - /// See Modeling entity types and relationships, and - /// Accessing SQL Server and SQL Azure databases with EF Core - /// for more information and examples. - /// - /// The type of the property being configured. - /// The builder for the property being configured. - /// The name of the sequence. - /// The schema of the sequence. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseSequence( - this PropertyBuilder propertyBuilder, - string? name = null, - string? schema = null) - => (PropertyBuilder)UseSequence((PropertyBuilder)propertyBuilder, name, schema); - - /// - /// Configures the database sequence used for the key value generation pattern to generate values for the key property, - /// when targeting PostgreSQL. - /// - /// The builder for the property being configured. - /// The name of the sequence. - /// The schema of the sequence. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the sequence. - public static IConventionSequenceBuilder? HasSequence( - this IConventionPropertyBuilder propertyBuilder, - string? name, - string? schema, - bool fromDataAnnotation = false) - { - if (!propertyBuilder.CanSetSequence(name, schema, fromDataAnnotation)) - { - return null; - } - - propertyBuilder.Metadata.SetSequenceName(name, fromDataAnnotation); - propertyBuilder.Metadata.SetSequenceSchema(schema, fromDataAnnotation); - - return name == null - ? null - : propertyBuilder.Metadata.DeclaringType.Model.Builder.HasSequence(name, schema, fromDataAnnotation); - } - - /// - /// Returns a value indicating whether the given name and schema can be set for the key value generation sequence. - /// - /// The builder for the property being configured. - /// The name of the sequence. - /// The schema of the sequence. - /// Indicates whether the configuration was specified using a data annotation. - /// if the given name and schema can be set for the key value generation sequence. - public static bool CanSetSequence( - this IConventionPropertyBuilder propertyBuilder, - string? name, - string? schema, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - return propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceName, name, fromDataAnnotation) - && propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.SequenceSchema, schema, fromDataAnnotation); - } - - #endregion Sequence - - #region Serial - - /// - /// Configures the property to use the PostgreSQL SERIAL feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// - /// - /// This option should be considered deprecated starting with PostgreSQL 10, consider using instead. - /// - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseSerialColumn( - this PropertyBuilder propertyBuilder) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - var property = propertyBuilder.Metadata; - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SerialColumn); - property.SetSequenceName(null); - property.SetSequenceSchema(null); - property.RemoveHiLoOptions(); - property.RemoveIdentityOptions(); - - return propertyBuilder; - } - - /// - /// Configures the property to use the PostgreSQL SERIAL feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// - /// - /// This option should be considered deprecated starting with PostgreSQL 10, consider using instead. - /// - /// The type of the property being configured. - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseSerialColumn( - this PropertyBuilder propertyBuilder) - => (PropertyBuilder)UseSerialColumn((PropertyBuilder)propertyBuilder); - - #endregion Serial - - #region Identity always - - /// - /// - /// Configures the property to use the PostgreSQL IDENTITY feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// Values for this property will always be generated as identity, and the application will not be able - /// to override this behavior by providing a value. - /// - /// Available only starting PostgreSQL 10. - /// - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseIdentityAlwaysColumn(this PropertyBuilder propertyBuilder) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - var property = propertyBuilder.Metadata; - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); - property.SetSequenceName(null); - property.SetSequenceSchema(null); - - return propertyBuilder; - } - - /// - /// - /// Configures the property to use the PostgreSQL IDENTITY feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// Values for this property will always be generated as identity, and the application will not be able - /// to override this behavior by providing a value. - /// - /// Available only starting PostgreSQL 10. - /// - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseIdentityAlwaysColumn( - this PropertyBuilder propertyBuilder) - => (PropertyBuilder)UseIdentityAlwaysColumn((PropertyBuilder)propertyBuilder); - - #endregion Identity always - - #region Identity by default - - /// - /// - /// Configures the property to use the PostgreSQL IDENTITY feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// Values for this property will be generated as identity by default, but the application will be able - /// to override this behavior by providing a value. - /// - /// - /// This is the default behavior when targeting PostgreSQL. Available only starting PostgreSQL 10. - /// - /// - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseIdentityByDefaultColumn(this PropertyBuilder propertyBuilder) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - var property = propertyBuilder.Metadata; - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); - property.SetSequenceName(null); - property.SetSequenceSchema(null); - - return propertyBuilder; - } - - /// - /// - /// Configures the property to use the PostgreSQL IDENTITY feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// Values for this property will be generated as identity by default, but the application will be able - /// to override this behavior by providing a value. - /// - /// - /// This is the default behavior when targeting PostgreSQL. Available only starting PostgreSQL 10. - /// - /// - /// The type of the property being configured. - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseIdentityByDefaultColumn( - this PropertyBuilder propertyBuilder) - => (PropertyBuilder)UseIdentityByDefaultColumn((PropertyBuilder)propertyBuilder); - - /// - /// - /// Configures the property to use the PostgreSQL IDENTITY feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// Values for this property will be generated as identity by default, but the application will be able - /// to override this behavior by providing a value. - /// - /// - /// This internally calls . - /// This is the default behavior when targeting PostgreSQL. Available only starting PostgreSQL 10. - /// - /// - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseIdentityColumn( - this PropertyBuilder propertyBuilder) - => propertyBuilder.UseIdentityByDefaultColumn(); - - /// - /// - /// Configures the property to use the PostgreSQL IDENTITY feature to generate values for new entities, - /// when targeting PostgreSQL. This method sets the property to be . - /// Values for this property will be generated as identity by default, but the application will be able - /// to override this behavior by providing a value. - /// - /// - /// This internally calls . - /// This is the default behavior when targeting PostgreSQL. Available only starting PostgreSQL 10. - /// - /// - /// The type of the property being configured. - /// The builder for the property being configured. - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder UseIdentityColumn( - this PropertyBuilder propertyBuilder) - => propertyBuilder.UseIdentityByDefaultColumn(); - - #endregion Identity by default - - #region General value generation strategy - - /// - /// Configures the value generation strategy for the key property, when targeting PostgreSQL. - /// - /// The builder for the property being configured. - /// The value generation strategy. - /// Indicates whether the configuration was specified using a data annotation. - /// - /// The same builder instance if the configuration was applied, null otherwise. - /// - public static IConventionPropertyBuilder? HasValueGenerationStrategy( - this IConventionPropertyBuilder propertyBuilder, - NpgsqlValueGenerationStrategy? valueGenerationStrategy, - bool fromDataAnnotation = false) - { - if (propertyBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation)) - { - propertyBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, fromDataAnnotation); - - if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.SequenceHiLo) - { - propertyBuilder.HasHiLoSequence(null, null, fromDataAnnotation); - } - - if (valueGenerationStrategy != NpgsqlValueGenerationStrategy.Sequence) - { - propertyBuilder.HasSequence(null, null, fromDataAnnotation); - } - - return propertyBuilder; - } - - 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. - /// - /// The builder for the property being configured. - /// The value generation strategy. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the given value can be set as the default value generation strategy. - public static bool CanSetValueGenerationStrategy( - this IConventionPropertyBuilder propertyBuilder, - NpgsqlValueGenerationStrategy? valueGenerationStrategy, - bool fromDataAnnotation = false) - => propertyBuilder.CanSetAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); - - #endregion General value generation strategy - - #region Identity options - - /// - /// Sets the sequence options on an identity column. The column must be set as identity via - /// or . - /// - /// The builder for the property being configured. - /// - /// The starting value for the sequence. - /// The default starting value is for ascending sequences and for - /// descending ones. - /// - /// The amount to increment between values. Defaults to 1. - /// - /// The minimum value for the sequence. - /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. - /// - /// - /// The maximum value for the sequence. - /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. - /// - /// - /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. - /// Defaults to false. - /// - /// - /// Specifies how many sequence numbers are to be pre0allocated and stored in memory for faster access. - /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. - /// - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder HasIdentityOptions( - this PropertyBuilder propertyBuilder, - long? startValue = null, - long? incrementBy = null, - long? minValue = null, - long? maxValue = null, - bool? cyclic = null, - long? numbersToCache = null) - { - var property = propertyBuilder.Metadata; - property.SetIdentityStartValue(startValue); - property.SetIdentityIncrementBy(incrementBy); - property.SetIdentityMinValue(minValue); - property.SetIdentityMaxValue(maxValue); - property.SetIdentityIsCyclic(cyclic); - property.SetIdentityNumbersToCache(numbersToCache); - return propertyBuilder; - } - - /// - /// Sets the sequence options on an identity column. The column must be set as identity via - /// or . - /// - /// The builder for the property being configured. - /// - /// The starting value for the sequence. - /// The default starting value is for ascending sequences and for descending - /// ones. - /// - /// The amount to increment between values. Defaults to 1. - /// - /// The minimum value for the sequence. - /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. - /// - /// - /// The maximum value for the sequence. - /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. - /// - /// - /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. - /// Defaults to false. - /// - /// - /// Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. - /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. - /// - /// The same builder instance so that multiple calls can be chained. - public static PropertyBuilder HasIdentityOptions( - this PropertyBuilder propertyBuilder, - long? startValue = null, - long? incrementBy = null, - long? minValue = null, - long? maxValue = null, - bool? cyclic = null, - long? numbersToCache = null) - => (PropertyBuilder)HasIdentityOptions( - (PropertyBuilder)propertyBuilder, startValue, incrementBy, minValue, maxValue, cyclic, numbersToCache); - - /// - /// Sets the sequence options on an identity column. The column must be set as identity via - /// or . - /// - /// The builder for the property being configured. - /// - /// The starting value for the sequence. - /// The default starting value is for ascending sequences and for descending - /// ones. - /// - /// The amount to increment between values. Defaults to 1. - /// - /// The minimum value for the sequence. - /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. - /// - /// - /// The maximum value for the sequence. - /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. - /// - /// - /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. - /// Defaults to false. - /// - /// - /// Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. - /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. - /// - /// The same builder instance so that multiple calls can be chained. - public static IConventionPropertyBuilder? HasIdentityOptions( - this IConventionPropertyBuilder propertyBuilder, - long? startValue = null, - long? incrementBy = null, - long? minValue = null, - long? maxValue = null, - bool? cyclic = null, - long? numbersToCache = null) - { - if (propertyBuilder.CanSetIdentityOptions(startValue, incrementBy, minValue, maxValue, cyclic, numbersToCache)) - { - var property = propertyBuilder.Metadata; - property.SetIdentityStartValue(startValue); - property.SetIdentityIncrementBy(incrementBy); - property.SetIdentityMinValue(minValue); - property.SetIdentityMaxValue(maxValue); - property.SetIdentityIsCyclic(cyclic); - property.SetIdentityNumbersToCache(numbersToCache); - return propertyBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the sequence options can be set on the identity column. - /// - /// The builder for the property being configured. - /// - /// The starting value for the sequence. The default starting value is for ascending sequences and - /// for descending ones. - /// - /// The amount to increment between values. Defaults to 1. - /// - /// The minimum value for the sequence. - /// The default for an ascending sequence is 1. The default for a descending sequence is the minimum value of the data type. - /// - /// - /// The maximum value for the sequence. - /// The default for an ascending sequence is the maximum value of the data type. The default for a descending sequence is -1. - /// - /// - /// Sets whether or not the sequence will start again from the beginning once the maximum value is reached. - /// Defaults to false. - /// - /// - /// Specifies how many sequence numbers are to be pre-allocated and stored in memory for faster access. - /// The minimum value is 1 (only one value can be generated at a time, i.e., no cache), and this is also the default. - /// - /// The same builder instance so that multiple calls can be chained. - public static bool CanSetIdentityOptions( - this IConventionPropertyBuilder propertyBuilder, - long? startValue = null, - long? incrementBy = null, - long? minValue = null, - long? maxValue = null, - bool? cyclic = null, - long? numbersToCache = null) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - var value = new IdentitySequenceOptionsData - { - StartValue = startValue, - IncrementBy = incrementBy ?? 1, - MinValue = minValue, - MaxValue = maxValue, - IsCyclic = cyclic ?? false, - NumbersToCache = numbersToCache ?? 1 - }.Serialize(); - - return propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.IdentityOptions, value); - } - - #endregion Identity options - - #region Array value conversion - - /// - /// Configures a PostgreSQL array conversion. - /// - [Obsolete( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", - error: true)] - public static PropertyBuilder HasPostgresArrayConversion( - this PropertyBuilder propertyBuilder, - Expression> convertToProviderExpression, - Expression> convertFromProviderExpression) - => throw new NotSupportedException( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); - - /// - /// Configures a PostgreSQL array conversion. - /// - [Obsolete( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", - error: true)] - public static PropertyBuilder> HasPostgresArrayConversion( - this PropertyBuilder> propertyBuilder, - Expression> convertToProviderExpression, - Expression> convertFromProviderExpression) - => throw new NotSupportedException( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); - - /// - /// Configures a PostgreSQL array conversion. - /// - [Obsolete( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", - error: true)] - public static PropertyBuilder HasPostgresArrayConversion( - this PropertyBuilder propertyBuilder, - ValueConverter elementValueConverter) - => throw new NotSupportedException( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); - - /// - /// Configures a PostgreSQL array conversion. - /// - [Obsolete( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html", - error: true)] - public static PropertyBuilder> HasPostgresArrayConversion( - this PropertyBuilder> propertyBuilder, - ValueConverter elementValueConverter) - => throw new NotSupportedException( - "HasPostgresArrayConversion has been replaced with the standard EF 8 primitive collection API, see https://www.npgsql.org/efcore/release-notes/8.0.html"); - - #endregion Array value conversion - - #region Generated tsvector column - - // Note: tsvector properties can be configured with a generic API through the entity type builder - - /// - /// Configures the property to be a full-text search tsvector column over the given properties. - /// - /// The builder for the property 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. - /// - /// - /// An array of property names to be included in the tsvector. - /// A builder to further configure the property. - public static PropertyBuilder IsGeneratedTsVectorColumn( - this PropertyBuilder propertyBuilder, - string config, - params string[] includedPropertyNames) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - Check.NotNull(config, nameof(config)); - Check.NotEmpty(includedPropertyNames, nameof(includedPropertyNames)); - - propertyBuilder.HasColumnType("tsvector"); - propertyBuilder.Metadata.SetTsVectorConfig(config); - propertyBuilder.Metadata.SetTsVectorProperties(includedPropertyNames); - - return propertyBuilder; - } - - /// - /// Configures the property to be a full-text search tsvector column over the given properties. - /// - /// The builder for the property 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. - /// - /// - /// An array of property names to be included in the tsvector. - /// A builder to further configure the property. - public static PropertyBuilder IsGeneratedTsVectorColumn( - this PropertyBuilder propertyBuilder, - string config, - params string[] includedPropertyNames) - => (PropertyBuilder)IsGeneratedTsVectorColumn((PropertyBuilder)propertyBuilder, config, includedPropertyNames); - - /// - /// Configures the property to be a full-text search tsvector column over the given properties. - /// - /// The builder for the property being configured. - /// - /// - /// The text search configuration for this generated tsvector property. - /// - /// - /// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information. - /// - /// - /// An array of property names to be included in the tsvector. - /// Indicates whether the configuration was specified using a data annotation. - /// - /// The same builder instance if the configuration was applied, - /// null otherwise. - /// - public static IConventionPropertyBuilder? IsGeneratedTsVectorColumn( - this IConventionPropertyBuilder propertyBuilder, - string config, - IReadOnlyList includedPropertyNames, - bool fromDataAnnotation = false) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - if (propertyBuilder.CanSetIsGeneratedTsVectorColumn(config, includedPropertyNames, fromDataAnnotation)) - { - propertyBuilder.HasColumnType("tsvector"); - propertyBuilder.Metadata.SetTsVectorConfig(config, fromDataAnnotation); - propertyBuilder.Metadata.SetTsVectorProperties(includedPropertyNames, fromDataAnnotation); - - return propertyBuilder; - } - - return null; - } - - /// - /// Returns a value indicating whether the property can be configured as a full-text search tsvector column. - /// - /// The builder for the property 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. - /// - /// - /// An array of property names to be included in the tsvector. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the property can be configured as a full-text search tsvector column. - public static bool CanSetIsGeneratedTsVectorColumn( - this IConventionPropertyBuilder propertyBuilder, - string? config, - IReadOnlyList? includedPropertyNames, - bool fromDataAnnotation = false) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - return (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(propertyBuilder.Metadata.GetTsVectorConfigConfigurationSource()) - && (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(propertyBuilder.Metadata.GetTsVectorPropertiesConfigurationSource()) - || config == propertyBuilder.Metadata.GetTsVectorConfig() - && StructuralComparisons.StructuralEqualityComparer.Equals( - includedPropertyNames, propertyBuilder.Metadata.GetTsVectorProperties()); - } - - #endregion Generated tsvector column - - #region Compression method - - /// - /// Sets the compression method for the column. - /// - /// This feature was introduced in PostgreSQL 14. - /// The builder for the property being configured. - /// The compression method. - /// A builder to further configure the property. - public static PropertyBuilder UseCompressionMethod( - this PropertyBuilder propertyBuilder, - string? compressionMethod) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - Check.NullButNotEmpty(compressionMethod, nameof(compressionMethod)); - - propertyBuilder.Metadata.SetCompressionMethod(compressionMethod); - - return propertyBuilder; - } - - /// - /// Sets the compression method for the column. - /// - /// This feature was introduced in PostgreSQL 14. - /// The builder for the property being configured. - /// The compression method. - /// A builder to further configure the property. - public static PropertyBuilder UseCompressionMethod( - this PropertyBuilder propertyBuilder, - string? compressionMethod) - => (PropertyBuilder)UseCompressionMethod((PropertyBuilder)propertyBuilder, compressionMethod); - - /// - /// Sets the compression method for the column. - /// - /// This feature was introduced in PostgreSQL 14. - /// The builder for the property being configured. - /// The compression method. - /// Indicates whether the configuration was specified using a data annotation. - /// A builder to further configure the property. - public static IConventionPropertyBuilder? UseCompressionMethod( - this IConventionPropertyBuilder propertyBuilder, - string? compressionMethod, - bool fromDataAnnotation = false) - { - if (propertyBuilder.CanSetCompressionMethod(compressionMethod, fromDataAnnotation)) - { - propertyBuilder.Metadata.SetCompressionMethod(compressionMethod, fromDataAnnotation); - - return propertyBuilder; - } - - return null; - } - - /// - /// Whether the compression method for the column can be set. - /// - /// This feature was introduced in PostgreSQL 14. - /// The builder for the property being configured. - /// The compression method. - /// Indicates whether the configuration was specified using a data annotation. - /// true if the index can be configured with the method - public static bool CanSetCompressionMethod( - this IConventionPropertyBuilder propertyBuilder, - string? compressionMethod, - bool fromDataAnnotation = false) - { - Check.NotNull(propertyBuilder, nameof(propertyBuilder)); - - return propertyBuilder.CanSetAnnotation(NpgsqlAnnotationNames.CompressionMethod, compressionMethod, fromDataAnnotation); - } - - #endregion Compression method -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs deleted file mode 100644 index fae6ab697f..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlAggregateDbFunctionsExtensions.cs +++ /dev/null @@ -1,503 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods supporting aggregate function translation for PostgreSQL. -/// -public static class NpgsqlAggregateDbFunctionsExtensions -{ - /// - /// Collects all the input values, including nulls, into a PostgreSQL array. - /// Corresponds to the PostgreSQL array_agg aggregate function. - /// - /// The instance. - /// The input values to be aggregated into an array. - /// PostgreSQL documentation for aggregate functions. - public static T[] ArrayAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ArrayAgg))); - - /// - /// Collects all the input values, including nulls, into a json array. Values are converted to JSON as per to_json or - /// to_jsonb. Corresponds to the PostgreSQL json_agg aggregate function. - /// - /// The instance. - /// The input values to be aggregated into a JSON array. - /// PostgreSQL documentation for aggregate functions. - public static T[] JsonAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonAgg))); - - /// - /// Collects all the input values, including nulls, into a jsonb array. Values are converted to JSON as per to_json or - /// to_jsonb. Corresponds to the PostgreSQL jsonb_agg aggregate function. - /// - /// The instance. - /// The input values to be aggregated into a JSON array. - /// PostgreSQL documentation for aggregate functions. - public static T[] JsonbAgg(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonbAgg))); - - /// - /// Computes the sum of the non-null input intervals. Corresponds to the PostgreSQL sum aggregate function. - /// - /// The instance. - /// The input values to be summed. - /// PostgreSQL documentation for aggregate functions. - public static TimeSpan? Sum(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Sum))); - - /// - /// Computes the average (arithmetic mean) of the non-null input intervals. Corresponds to the PostgreSQL avg aggregate function. - /// - /// The instance. - /// The input values to be computed into an average. - /// PostgreSQL documentation for aggregate functions. - public static TimeSpan? Average(this DbFunctions _, IEnumerable input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Average))); - - // See additional range aggregate functions in NpgsqlRangeDbfunctionsExtensions - - #region JsonObjectAgg - - /// - /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per - /// to_json. Values can be , but not keys. - /// Corresponds to the PostgreSQL json_object_agg aggregate function. - /// - /// The instance. - /// An enumerable of key-value pairs to be aggregated into a JSON object. - /// PostgreSQL documentation for aggregate functions. - public static string JsonObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonObjectAgg))); - - /// - /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per - /// to_json. Values can be , but not keys. - /// Corresponds to the PostgreSQL json_object_agg aggregate function. - /// - /// The instance. - /// An enumerable of key-value pairs to be aggregated into a JSON object. - /// PostgreSQL documentation for aggregate functions. - public static TReturn JsonObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonObjectAgg))); - - /// - /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per - /// to_jsonb. Values can be , but not keys. - /// Corresponds to the PostgreSQL jsonb_object_agg aggregate function. - /// - /// The instance. - /// An enumerable of key-value pairs to be aggregated into a JSON object. - /// PostgreSQL documentation for aggregate functions. - public static string JsonbObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonbObjectAgg))); - - /// - /// Collects all the key/value pairs into a JSON object. Key arguments are coerced to text; value arguments are converted as per - /// to_jsonb. Values can be , but not keys. - /// Corresponds to the PostgreSQL jsonb_object_agg aggregate function. - /// - /// The instance. - /// An enumerable of key-value pairs to be aggregated into a JSON object. - /// PostgreSQL documentation for aggregate functions. - public static TReturn JsonbObjectAgg(this DbFunctions _, IEnumerable<(T1, T2)> keyValuePairs) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonbObjectAgg))); - - #endregion JsonObjectAgg - - #region Sample standard deviation - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - /// - /// Returns the sample standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_samp function. - /// - /// The instance. - /// The values. - /// The computed sample standard deviation. - public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); - - #endregion Sample standard deviation - - #region Population standard deviation - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - /// - /// Returns the population standard deviation of all values in the specified expression. - /// Corresponds to the PostgreSQL stddev_pop function. - /// - /// The instance. - /// The values. - /// The computed population standard deviation. - public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); - - #endregion Population standard deviation - - #region Sample variance - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - /// - /// Returns the sample variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_samp function. - /// - /// The instance. - /// The values. - /// The computed sample variance. - public static double? VarianceSample(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); - - #endregion Sample variance - - #region Population variance - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - /// - /// Returns the population variance of all values in the specified expression. - /// Corresponds to the PostgreSQL var_pop function. - /// - /// The instance. - /// The values. - /// The computed population variance. - public static double? VariancePopulation(this DbFunctions _, IEnumerable values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); - - #endregion Population variance - - #region Other statistics functions - - /// - /// Computes the correlation coefficient. Corresponds to the PostgreSQL corr function. - /// - /// The instance. - /// The values. - public static double? Correlation(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Correlation))); - - /// - /// Computes the population covariance. Corresponds to the PostgreSQL covar_pop function. - /// - /// The instance. - /// The values. - public static double? CovariancePopulation(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CovariancePopulation))); - - /// - /// Computes the sample covariance. Corresponds to the PostgreSQL covar_samp function. - /// - /// The instance. - /// The values. - public static double? CovarianceSample(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CovarianceSample))); - - /// - /// Computes the average of the independent variable, sum(X)/N. - /// Corresponds to the PostgreSQL regr_avgx function. - /// - /// The instance. - /// The values. - public static double? RegrAverageX(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrAverageX))); - - /// - /// Computes the average of the dependent variable, sum(Y)/N. - /// Corresponds to the PostgreSQL regr_avgy function. - /// - /// The instance. - /// The values. - public static double? RegrAverageY(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrAverageY))); - - /// - /// Computes the number of rows in which both inputs are non-null. - /// Corresponds to the PostgreSQL regr_count function. - /// - /// The instance. - /// The values. - public static long? RegrCount(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrCount))); - - /// - /// Computes the y-intercept of the least-squares-fit linear equation determined by the (X, Y) pairs. - /// Corresponds to the PostgreSQL regr_intercept function. - /// - /// The instance. - /// The values. - public static double? RegrIntercept(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrIntercept))); - - /// - /// Computes the square of the correlation coefficient. - /// Corresponds to the PostgreSQL regr_r2 function. - /// - /// The instance. - /// The values. - public static double? RegrR2(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrR2))); - - /// - /// Computes the slope of the least-squares-fit linear equation determined by the (X, Y) pairs. - /// Corresponds to the PostgreSQL regr_slope function. - /// - /// The instance. - /// The values. - public static double? RegrSlope(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrSlope))); - - /// - /// Computes the “sum of squares” of the independent variable, sum(X^2) - sum(X)^2/N. - /// Corresponds to the PostgreSQL regr_sxx function. - /// - /// The instance. - /// The values. - public static double? RegrSXX(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrSXX))); - - /// - /// Computes the “sum of products” of independent times dependent variables, sum(X*Y) - sum(X) * sum(Y)/N. - /// Corresponds to the PostgreSQL regr_sxy function. - /// - /// The instance. - /// The values. - public static double? RegrSXY(this DbFunctions _, IEnumerable<(double, double)> values) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RegrSXY))); - - #endregion Other statistics functions -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs deleted file mode 100644 index 87ddba3017..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlCubeDbFunctionsExtensions.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ReSharper disable once CheckNamespace - -using NpgsqlTypes; - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods for supporting PostgreSQL translation. -/// -/// -/// See PostgreSQL documentation for the cube extension. -/// -public static class NpgsqlCubeDbFunctionsExtensions -{ - /// - /// Determines whether two cubes overlap (have points in common). - /// - /// The first cube. - /// The second cube. - /// - /// true if the cubes overlap; otherwise, false. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Overlaps(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); - - /// - /// Determines whether a cube contains another cube. - /// - /// The cube to check. - /// The cube that may be contained. - /// - /// true if contains ; otherwise, false. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Contains(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a cube is contained by another cube. - /// - /// The cube to check. - /// The cube that may contain it. - /// - /// true if is contained by ; otherwise, false. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool ContainedBy(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - /// - /// Extracts the n-th coordinate of the cube. - /// - /// The cube. - /// The coordinate index to extract. - /// The coordinate value at the specified index. - /// - /// This method uses zero-based indexing (C# convention), which is translated to PostgreSQL's one-based indexing. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static double NthCoordinate(this NpgsqlCube cube, int index) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinate))); - - /// - /// Extracts the n-th coordinate of the cube for K-nearest neighbor (KNN) indexing. - /// - /// The cube. - /// The coordinate index to extract. - /// The coordinate value at the specified index. - /// - /// - /// This method uses zero-based indexing (C# convention), which is translated to PostgreSQL's one-based indexing. - /// - /// - /// This is the same as except it is marked "lossy" for GiST indexing purposes, - /// which is useful for K-nearest neighbor queries. - /// - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static double NthCoordinateKnn(this NpgsqlCube cube, int index) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NthCoordinateKnn))); - - /// - /// Computes the Euclidean distance between two cubes. - /// - /// The first cube. - /// The second cube. - /// The Euclidean distance between the two cubes. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static double Distance(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - /// - /// Computes the taxicab (L-1 metric) distance between two cubes. - /// - /// The first cube. - /// The second cube. - /// The taxicab distance between the two cubes. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static double DistanceTaxicab(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceTaxicab))); - - /// - /// Computes the Chebyshev (L-inf metric) distance between two cubes. - /// - /// The first cube. - /// The second cube. - /// The Chebyshev distance between the two cubes. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static double DistanceChebyshev(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DistanceChebyshev))); - - /// - /// Computes the union of two cubes, producing the smallest cube that encloses both. - /// - /// The first cube. - /// The second cube. - /// The smallest cube that encloses both input cubes. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlCube Union(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); - - /// - /// Computes the intersection of two cubes. - /// - /// The first cube. - /// The second cube. - /// The intersection of the two cubes. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlCube Intersect(this NpgsqlCube cube, NpgsqlCube other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); - - /// - /// Increases the size of a cube by a specified radius in at least the specified number of dimensions. - /// - /// The cube to enlarge. - /// The amount by which to enlarge the cube (can be negative to shrink). - /// The number of dimensions to enlarge (optional, defaults to all dimensions). - /// The enlarged (or shrunk) cube. - /// - /// If the specified number of dimensions is greater than the cube's current dimensions, - /// the extra dimensions are added with the specified radius. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlCube Enlarge(this NpgsqlCube cube, double radius, int dimensions) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Enlarge))); -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs deleted file mode 100644 index 868faa7e2e..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlDbFunctionsExtensions.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System.Runtime.CompilerServices; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides Npgsql-specific extension methods on . -/// -public static class NpgsqlDbFunctionsExtensions -{ - // ReSharper disable once InconsistentNaming - /// - /// An implementation of the PostgreSQL ILIKE operation, which is an insensitive LIKE. - /// - /// The instance. - /// The string that is to be matched. - /// The pattern which may involve wildcards %,_,[,],^. - /// if there is a match. - public static bool ILike(this DbFunctions _, string matchExpression, string pattern) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ILike))); - - // ReSharper disable once InconsistentNaming - /// - /// An implementation of the PostgreSQL ILIKE operation, which is an insensitive LIKE. - /// - /// The instance. - /// The string that is to be matched. - /// The pattern which may involve wildcards %,_,[,],^. - /// - /// The escape character (as a single character string) to use in front of %,_,[,],^ - /// if they are not used as wildcards. - /// - /// if there is a match. - public static bool ILike(this DbFunctions _, string matchExpression, string pattern, string escapeCharacter) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ILike))); - - /// - /// Splits at occurrences of delimiter and forms the resulting fields into a text array. - /// - /// The instance. - /// The string to be split. - /// - /// If null, each character in the string will become a separate element in the array. - /// If an empty string, the string is treated as a single field. - /// - /// - public static string[] StringToArray(this DbFunctions _, string value, string delimiter) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StringToArray))); - - /// - /// Splits at occurrences of delimiter and forms the resulting fields into a text array. - /// - /// The instance. - /// The string to be split. - /// - /// If null, each character in the string will become a separate element in the array. - /// If an empty string, the string is treated as a single field. - /// - /// Fields matching this value string are replaced by null. - public static string[] StringToArray(this DbFunctions _, string value, string delimiter, string nullString) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StringToArray))); - - /// - /// Reverses a string by calling PostgreSQL reverse(). - /// - /// The instance. - /// The string that is to be reversed. - /// The reversed string. - public static string Reverse(this DbFunctions _, string value) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Reverse))); - - /// - /// Returns whether the row value represented by is greater than the row value represented by - /// . - /// - /// - /// For more information on row value comparisons, see - /// - /// the PostgreSQL documentation. - /// - /// - public static bool GreaterThan(this DbFunctions _, ITuple a, ITuple b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan))); - - /// - /// Returns whether the row value represented by is less than the row value represented by . - /// - /// - /// For more information on row value comparisons, see - /// - /// the PostgreSQL documentation. - /// - /// - public static bool LessThan(this DbFunctions _, ITuple a, ITuple b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan))); - - /// - /// Returns whether the row value represented by is greater than or equal to the row value represented by - /// . - /// - /// - /// For more information on row value comparisons, see - /// - /// the PostgreSQL documentation. - /// - /// - public static bool GreaterThanOrEqual(this DbFunctions _, ITuple a, ITuple b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual))); - - /// - /// Returns whether the row value represented by is less than or equal to the row value represented by - /// . - /// - /// - /// For more information on row value comparisons, see - /// - /// the PostgreSQL documentation. - /// - /// - public static bool LessThanOrEqual(this DbFunctions _, ITuple a, ITuple b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual))); - - /// - /// Returns the distance between two dates as a number of days, particularly suitable for sorting where the appropriate index is - /// defined. - /// - /// - /// This requires the btree_gist built-in PostgreSQL extension, see - /// . - /// - public static int Distance(this DbFunctions _, DateOnly a, DateOnly b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Distance))); - - /// - /// Returns the distance between two timestamps as a PostgreSQL interval, particularly suitable for sorting where the appropriate - /// index is defined. - /// - /// - /// This requires the btree_gist built-in PostgreSQL extension, see - /// . - /// - 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/DbFunctionsExtensions/NpgsqlFullTextSearchDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFullTextSearchDbFunctionsExtensions.cs deleted file mode 100644 index 0aa9cd1d08..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFullTextSearchDbFunctionsExtensions.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. -/// The methods on this class are accessed via . -/// -/// -/// See Database functions. -/// -[SuppressMessage("ReSharper", "UnusedParameter.Global")] -public static class NpgsqlFullTextSearchDbFunctionsExtensions -{ - /// - /// Convert to a tsvector. - /// - /// - /// https://www.postgresql.org/docs/current/static/functions-textsearch.html - /// - public static NpgsqlTsVector ArrayToTsVector(this DbFunctions _, string[] lexemes) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ArrayToTsVector))); - - /// - /// Reduce to tsvector. - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-DOCUMENTS - /// - public static NpgsqlTsVector ToTsVector(this DbFunctions _, string document) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsVector))); - - /// - /// Reduce to tsvector using the text search configuration specified - /// by . - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-DOCUMENTS - /// - public static NpgsqlTsVector ToTsVector(this DbFunctions _, [NotParameterized] string config, string document) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsVector))); - - /// - /// Produce tsquery from ignoring punctuation. - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery PlainToTsQuery(this DbFunctions _, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PlainToTsQuery))); - - /// - /// Produce tsquery from ignoring punctuation and using the text search - /// configuration specified by . - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery PlainToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PlainToTsQuery))); - - /// - /// Produce tsquery that searches for a phrase from ignoring punctuation. - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery PhraseToTsQuery(this DbFunctions _, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PhraseToTsQuery))); - - /// - /// Produce tsquery that searches for a phrase from ignoring punctuation - /// and using the text search configuration specified by . - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery PhraseToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(PhraseToTsQuery))); - - /// - /// Normalize words in and convert to tsquery. If your input - /// contains punctuation that should not be treated as text search operators, use - /// instead. - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery ToTsQuery(this DbFunctions _, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsQuery))); - - /// - /// Normalize words in and convert to tsquery using the text search - /// configuration specified by . If your input contains punctuation - /// that should not be treated as text search operators, use - /// instead. - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery ToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToTsQuery))); - - /// - /// Convert tsquery using the simplified websearch syntax. - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery WebSearchToTsQuery(this DbFunctions _, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(WebSearchToTsQuery))); - - /// - /// Convert tsquery using the simplified websearch syntax and the text - /// search configuration specified by . - /// - /// - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES - /// - public static NpgsqlTsQuery WebSearchToTsQuery(this DbFunctions _, [NotParameterized] string config, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(WebSearchToTsQuery))); - - /// - /// Returns a new string that removes diacritics from characters in the given . - /// - /// The instance. - /// A specific text search dictionary. - /// The text to remove the diacritics. - /// - /// The method call is translated to unaccent(regdictionary, text). - /// See https://www.postgresql.org/docs/current/unaccent.html. - /// - /// A string without diacritics. - public static string Unaccent(this DbFunctions _, string regDictionary, string text) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Unaccent))); - - /// - /// Returns a new string that removes diacritics from characters in the given . - /// - /// The instance. - /// The text to remove the diacritics. - /// - /// The method call is translated to unaccent(text). - /// See https://www.postgresql.org/docs/current/unaccent.html. - /// - /// A string without diacritics. - public static string Unaccent(this DbFunctions _, string text) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Unaccent))); -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFullTextSearchLinqExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFullTextSearchLinqExtensions.cs deleted file mode 100644 index 7da2030646..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFullTextSearchLinqExtensions.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides EF Core extension methods for Npgsql full-text search types. -/// -[SuppressMessage("ReSharper", "UnusedParameter.Global")] -public static class NpgsqlFullTextSearchLinqExtensions -{ - /// - /// AND tsquerys together. Generates the "&&" operator. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery And(this NpgsqlTsQuery query1, NpgsqlTsQuery query2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(And))); - - /// - /// OR tsquerys together. Generates the "||" operator. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery Or(this NpgsqlTsQuery query1, NpgsqlTsQuery query2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(Or))); - - /// - /// Negate a tsquery. Generates the "!!" operator. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery ToNegative(this NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(ToNegative))); - - /// - /// Returns whether contains . - /// Generates the "@>" operator. - /// http://www.postgresql.org/docs/current/static/functions-textsearch.html - /// - public static bool Contains(this NpgsqlTsQuery query1, NpgsqlTsQuery query2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(Contains))); - - /// - /// Returns whether is contained within . - /// Generates the "<@" operator. - /// http://www.postgresql.org/docs/current/static/functions-textsearch.html - /// - public static bool IsContainedIn(this NpgsqlTsQuery query1, NpgsqlTsQuery query2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(IsContainedIn))); - - /// - /// Returns the number of lexemes plus operators in . - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static int GetNodeCount(this NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(GetNodeCount))); - - /// - /// Get the indexable part of . - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static string GetQueryTree(this NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(GetQueryTree))); - - /// - /// Returns a string suitable for display containing a query match. - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE - /// - public static string GetResultHeadline(this NpgsqlTsQuery query, string document) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(GetResultHeadline))); - - /// - /// Returns a string suitable for display containing a query match. - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE - /// - public static string GetResultHeadline(this NpgsqlTsQuery query, string document, string options) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(GetResultHeadline))); - - /// - /// Returns a string suitable for display containing a query match using the text - /// search configuration specified by . - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE - /// - public static string GetResultHeadline(this NpgsqlTsQuery query, string config, string document, string options) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(GetResultHeadline))); - - /// - /// Searches for occurrences of , and replaces - /// each occurrence with a . All parameters are of type tsquery. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery Rewrite(this NpgsqlTsQuery query, NpgsqlTsQuery target, NpgsqlTsQuery substitute) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(Rewrite))); - - /// - /// For each row of the SQL result, occurrences of the first column value (the target) - /// are replaced by the second column value (the substitute) within the current value. - /// The must yield two columns of tsquery type. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery Rewrite(this NpgsqlTsQuery query, string select) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(Rewrite))); - - /// - /// Returns a tsquery that searches for a match to followed by a match - /// to . - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery ToPhrase(this NpgsqlTsQuery query1, NpgsqlTsQuery query2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(ToPhrase))); - - /// - /// Returns a tsquery that searches for a match to followed by a match - /// to at a distance of lexemes using - /// the <N> tsquery operator - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSQUERY - /// - public static NpgsqlTsQuery ToPhrase(this NpgsqlTsQuery query1, NpgsqlTsQuery query2, int distance) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsQuery) + "." + nameof(ToPhrase))); - - /// - /// This method generates the "@@" match operator. The parameter is - /// assumed to be a plain search query and will be converted to a tsquery using plainto_tsquery. - /// http://www.postgresql.org/docs/current/static/textsearch-intro.html#TEXTSEARCH-MATCHING - /// - public static bool Matches(this NpgsqlTsVector vector, string query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Matches))); - - /// - /// This method generates the "@@" match operator. - /// http://www.postgresql.org/docs/current/static/textsearch-intro.html#TEXTSEARCH-MATCHING - /// - public static bool Matches(this NpgsqlTsVector vector, NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Matches))); - - /// - /// Returns a vector which combines the lexemes and positional information of - /// and using the || tsvector operator. Positions and weight labels are retained - /// during the concatenation. - /// https://www.postgresql.org/docs/10/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static NpgsqlTsVector Concat(this NpgsqlTsVector vector1, NpgsqlTsVector vector2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Concat))); - - /// - /// Assign weight to each element of and return a new - /// weighted tsvector. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static NpgsqlTsVector SetWeight(this NpgsqlTsVector vector, NpgsqlTsVector.Lexeme.Weight weight) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(SetWeight))); - - /// - /// Assign weight to elements of that are in and - /// return a new weighted tsvector. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static NpgsqlTsVector SetWeight(this NpgsqlTsVector vector, NpgsqlTsVector.Lexeme.Weight weight, string[] lexemes) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(SetWeight))); - - /// - /// Assign weight to each element of and return a new - /// weighted tsvector. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static NpgsqlTsVector SetWeight(this NpgsqlTsVector vector, char weight) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(SetWeight))); - - /// - /// Assign weight to elements of that are in and - /// return a new weighted tsvector. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static NpgsqlTsVector SetWeight(this NpgsqlTsVector vector, char weight, string[] lexemes) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(SetWeight))); - - /// - /// Return a new vector with removed from - /// https://www.postgresql.org/docs/current/static/functions-textsearch.html - /// - public static NpgsqlTsVector Delete(this NpgsqlTsVector vector, string lexeme) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Delete))); - - /// - /// Return a new vector with removed from - /// https://www.postgresql.org/docs/current/static/functions-textsearch.html - /// - public static NpgsqlTsVector Delete(this NpgsqlTsVector vector, string[] lexemes) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Delete))); - - /// - /// Returns a new vector with only lexemes having weights specified in . - /// https://www.postgresql.org/docs/current/static/functions-textsearch.html - /// - public static NpgsqlTsVector Filter(this NpgsqlTsVector vector, char[] weights) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Filter))); - - /// - /// Returns the number of lexemes in . - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static int GetLength(this NpgsqlTsVector vector) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(GetLength))); - - /// - /// Removes weights and positions from and returns - /// a new stripped tsvector. - /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR - /// - public static NpgsqlTsVector ToStripped(this NpgsqlTsVector vector) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(ToStripped))); - - /// - /// Calculates the rank of for . - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float Rank(this NpgsqlTsVector vector, NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Rank))); - - /// - /// Calculates the rank of for while normalizing - /// the result according to the behaviors specified by . - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float Rank(this NpgsqlTsVector vector, NpgsqlTsQuery query, NpgsqlTsRankingNormalization normalization) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Rank))); - - /// - /// Calculates the rank of for with custom - /// weighting for word instances depending on their labels (D, C, B or A). - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float Rank(this NpgsqlTsVector vector, float[] weights, NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Rank))); - - /// - /// Calculates the rank of for while normalizing - /// the result according to the behaviors specified by - /// and using custom weighting for word instances depending on their labels (D, C, B or A). - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float Rank( - this NpgsqlTsVector vector, - float[] weights, - NpgsqlTsQuery query, - NpgsqlTsRankingNormalization normalization) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(Rank))); - - /// - /// Calculates the rank of for using the cover - /// density method. - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float RankCoverDensity(this NpgsqlTsVector vector, NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(RankCoverDensity))); - - /// - /// Calculates the rank of for using the cover - /// density method while normalizing the result according to the behaviors specified by - /// . - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float RankCoverDensity(this NpgsqlTsVector vector, NpgsqlTsQuery query, NpgsqlTsRankingNormalization normalization) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(RankCoverDensity))); - - /// - /// Calculates the rank of for using the cover - /// density method with custom weighting for word instances depending on their labels (D, C, B or A). - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float RankCoverDensity(this NpgsqlTsVector vector, float[] weights, NpgsqlTsQuery query) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(RankCoverDensity))); - - /// - /// Calculates the rank of for using the cover density - /// method while normalizing the result according to the behaviors specified by - /// and using custom weighting for word instances depending on their labels (D, C, B or A). - /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING - /// - public static float RankCoverDensity( - this NpgsqlTsVector vector, - float[] weights, - NpgsqlTsQuery query, - NpgsqlTsRankingNormalization normalization) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(NpgsqlTsVector) + "." + nameof(RankCoverDensity))); -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs deleted file mode 100644 index 4b463fb2c4..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. -/// The methods on this class are accessed via . -/// -/// -/// See Database functions. -/// -public static class NpgsqlFuzzyStringMatchDbFunctionsExtensions -{ - /// - /// The soundex function converts a string to its Soundex code. - /// - /// - /// The method call is translated to soundex(text). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static string FuzzyStringMatchSoundex(this DbFunctions _, string text) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchSoundex))); - - /// - /// The difference function converts two strings to their Soundex codes and - /// then returns the number of matching code positions. Since Soundex codes - /// have four characters, the result ranges from zero to four, with zero being - /// no match and four being an exact match. - /// - /// - /// The method call is translated to difference(source, target). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static int FuzzyStringMatchDifference(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDifference))); - - /// - /// Returns the Levenshtein distance between two strings. - /// - /// - /// The method call is translated to levenshtein(source, target). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static int FuzzyStringMatchLevenshtein(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshtein))); - - /// - /// Returns the Levenshtein distance between two strings. - /// - /// - /// The method call is translated to levenshtein(source, target, insertionCost, deletionCost, substitutionCost). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static int FuzzyStringMatchLevenshtein( - this DbFunctions _, - string source, - string target, - int insertionCost, - int deletionCost, - int substitutionCost) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshtein))); - - /// - /// levenshtein_less_equal is an accelerated version of the Levenshtein function for use when only small distances are of interest. - /// If the actual distance is less than or equal to maximum distance, then levenshtein_less_equal returns the correct distance; - /// otherwise it returns some value greater than maximum distance. If maximum distance is negative then the behavior is the same as - /// levenshtein. - /// - /// - /// The method call is translated to levenshtein_less_equal(source, target, maximumDistance). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static int FuzzyStringMatchLevenshteinLessEqual(this DbFunctions _, string source, string target, int maximumDistance) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshteinLessEqual))); - - /// - /// levenshtein_less_equal is an accelerated version of the Levenshtein function for use when only small distances are of interest. - /// If the actual distance is less than or equal to maximum distance, then levenshtein_less_equal returns the correct distance; - /// otherwise it returns some value greater than maximum distance. If maximum distance is negative then the behavior is the same as - /// levenshtein. - /// - /// - /// The method call is translated to - /// levenshtein_less_equal(source, target, insertionCost, deletionCost, substitutionCost, maximumDistance). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static int FuzzyStringMatchLevenshteinLessEqual( - this DbFunctions _, - string source, - string target, - int insertionCost, - int deletionCost, - int substitutionCost, - int maximumDistance) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchLevenshteinLessEqual))); - - /// - /// The metaphone function converts a string to its Metaphone code. - /// - /// - /// The method call is translated to metaphone(text, maximumOutputLength). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static string FuzzyStringMatchMetaphone(this DbFunctions _, string text, int maximumOutputLength) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchMetaphone))); - - /// - /// The dmetaphone function converts a string to its primary Double Metaphone code. - /// - /// - /// The method call is translated to dmetaphone(text). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static string FuzzyStringMatchDoubleMetaphone(this DbFunctions _, string text) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDoubleMetaphone))); - - /// - /// The dmetaphone_alt function converts a string to its alternate Double Metaphone code. - /// - /// - /// The method call is translated to dmetaphone_alt(text). - /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. - /// - public static string FuzzyStringMatchDoubleMetaphoneAlt(this DbFunctions _, string text) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDoubleMetaphoneAlt))); -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlJsonDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlJsonDbFunctionsExtensions.cs deleted file mode 100644 index 044760d629..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlJsonDbFunctionsExtensions.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Text.Json; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides methods for supporting translation to PostgreSQL JSON operators and functions. -/// -public static class NpgsqlJsonDbFunctionsExtensions -{ - /// - /// Checks if contains as top-level entries. - /// - /// DbFunctions instance - /// - /// A JSON column or value. Can be a , a string property mapped to JSON, - /// or a user POCO mapped to JSON. - /// - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// - /// This operation is only supported with PostgreSQL jsonb, not json. - /// See https://www.postgresql.org/docs/current/functions-json.html. - /// - public static bool JsonContains( - this DbFunctions _, - object json, - object contained) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonContains))); - - /// - /// Checks if is contained in as top-level entries. - /// - /// DbFunctions instance - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// - /// This operation is only supported with PostgreSQL jsonb, not json. - /// See https://www.postgresql.org/docs/current/functions-json.html. - /// - public static bool JsonContained( - this DbFunctions _, - object contained, - object json) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonContained))); - - /// - /// Checks if exists as a top-level key within . - /// - /// DbFunctions instance - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// A key to be checked inside . - /// - /// This operation is only supported with PostgreSQL jsonb, not json. - /// See https://www.postgresql.org/docs/current/functions-json.html. - /// - public static bool JsonExists(this DbFunctions _, object json, string key) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists))); - - /// - /// Checks if any of the given exist as top-level keys within . - /// - /// DbFunctions instance - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// A set of keys to be checked inside . - /// - /// This operation is only supported with PostgreSQL jsonb, not json. - /// See https://www.postgresql.org/docs/current/functions-json.html. - /// - public static bool JsonExistAny(this DbFunctions _, object json, params string[] keys) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExistAny))); - - /// - /// Checks if all of the given exist as top-level keys within . - /// - /// DbFunctions instance - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// A set of keys to be checked inside . - /// - /// This operation is only supported with PostgreSQL jsonb, not json. - /// See https://www.postgresql.org/docs/current/functions-json.html. - /// - public static bool JsonExistAll(this DbFunctions _, object json, params string[] keys) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExistAll))); - - /// - /// Returns the type of the outermost JSON value as a text string. - /// Possible types are object, array, string, number, boolean, and null. - /// - /// DbFunctions instance - /// - /// A JSON column or value. Can be a , a string, or a user POCO mapped to JSON. - /// - /// - /// See https://www.postgresql.org/docs/current/functions-json.html. - /// - public static string JsonTypeof(this DbFunctions _, object json) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonTypeof))); -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlMultirangeDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlMultirangeDbFunctionsExtensions.cs deleted file mode 100644 index d4905d7bc8..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlMultirangeDbFunctionsExtensions.cs +++ /dev/null @@ -1,768 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods for multiranges supporting PostgreSQL translation. -/// -public static class NpgsqlMultirangeDbFunctionsExtensions -{ - #region Contains - - /// - /// Determines whether a multirange contains a specified value. - /// - /// The multirange in which to locate the value. - /// The value to locate in the range. - /// - /// true - /// if the multirange contains the specified value; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Contains(this NpgsqlRange[] multirange, T value) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a multirange contains a specified value. - /// - /// The multirange in which to locate the value. - /// The value to locate in the range. - /// - /// true - /// if the multirange contains the specified value; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Contains(this List> multirange, T value) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a multirange contains a specified multirange. - /// - /// The multirange in which to locate the specified multirange. - /// The specified multirange to locate in the multirange. - /// - /// true - /// if the multirange contains the specified multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool Contains(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a multirange contains a specified multirange. - /// - /// The multirange in which to locate the specified multirange. - /// The specified multirange to locate in the multirange. - /// - /// true - /// if the multirange contains the specified multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static bool Contains(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a multirange contains a specified range. - /// - /// The multirange in which to locate the specified range. - /// The specified range to locate in the multirange. - /// - /// true - /// if the multirange contains the specified range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static bool Contains(this NpgsqlRange[] multirange1, NpgsqlRange multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a multirange contains a specified range. - /// - /// The multirange in which to locate the specified range. - /// The specified range to locate in the multirange. - /// - /// true - /// if the multirange contains the specified range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool Contains(this List> multirange1, NpgsqlRange multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - #endregion Contains - - #region ContainedBy - - /// - /// Determines whether a multirange is contained by a specified multirange. - /// - /// The specified multirange to locate in the multirange. - /// The multirange in which to locate the specified multirange. - /// - /// true - /// if the multirange contains the specified multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool ContainedBy(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - /// - /// Determines whether a multirange is contained by a specified multirange. - /// - /// The specified multirange to locate in the multirange. - /// The multirange in which to locate the specified multirange. - /// - /// true - /// if the multirange contains the specified multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool ContainedBy(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - /// - /// Determines whether a range is contained by a specified multirange. - /// - /// The specified range to locate in the multirange. - /// The multirange in which to locate the specified range. - /// - /// true - /// if the multirange contains the specified range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool ContainedBy(this NpgsqlRange range, NpgsqlRange[] multirange) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - /// - /// Determines whether a range is contained by a specified multirange. - /// - /// The specified range to locate in the multirange. - /// The multirange in which to locate the specified range. - /// - /// true - /// if the multirange contains the specified range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static bool ContainedBy(this NpgsqlRange range, List> multirange) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - #endregion ContainedBy - - #region Overlaps - - /// - /// Determines whether a multirange overlaps another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the multiranges overlap (share points in common); otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool Overlaps(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); - - /// - /// Determines whether a multirange overlaps another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the multiranges overlap (share points in common); otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool Overlaps(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); - - /// - /// Determines whether a multirange overlaps another range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange and range overlap (share points in common); otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static bool Overlaps(this NpgsqlRange[] multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); - - /// - /// Determines whether a multirange overlaps another range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange and range overlap (share points in common); otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool Overlaps(this List> multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); - - #endregion Overlaps - - #region IsStrictlyLeftOf - - /// - /// Determines whether a multirange is strictly to the left of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange is strictly to the left of the second multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static bool IsStrictlyLeftOf(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); - - /// - /// Determines whether a multirange is strictly to the left of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange is strictly to the left of the second multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part - /// of an EF Core LINQ query. - /// - public static bool IsStrictlyLeftOf(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); - - /// - /// Determines whether a multirange is strictly to the left of a range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange is strictly to the left of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static bool IsStrictlyLeftOf(this NpgsqlRange[] multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); - - /// - /// Determines whether a multirange is strictly to the left of a range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange is strictly to the left of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool IsStrictlyLeftOf(this List> multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); - - #endregion IsStrictlyLeftOf - - #region IsStrictlyRightOf - - /// - /// Determines whether a multirange is strictly to the right of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange is strictly to the right of the second multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool IsStrictlyRightOf(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); - - /// - /// Determines whether a multirange is strictly to the right of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange is strictly to the right of the second multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part - /// of an EF Core LINQ query. - /// - public static bool IsStrictlyRightOf(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); - - /// - /// Determines whether a multirange is strictly to the right of a range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange is strictly to the right of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static bool IsStrictlyRightOf(this NpgsqlRange[] multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); - - /// - /// Determines whether a multirange is strictly to the right of a range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange is strictly to the right of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool IsStrictlyRightOf(this List> multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); - - #endregion IsStrictlyRightOf - - #region DoesNotExtendLeftOf - - /// - /// Determines whether a multirange does not extend to the left of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange does not extend to the left of the multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool DoesNotExtendLeftOf(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); - - /// - /// Determines whether a multirange does not extend to the left of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange does not extend to the left of the multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as - /// part of an EF Core LINQ query. - /// - public static bool DoesNotExtendLeftOf(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); - - /// - /// Determines whether a multirange does not extend to the left of a range. - /// - /// The multirange. - /// The multirange. - /// - /// true - /// if the multirange does not extend to the left of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool DoesNotExtendLeftOf(this NpgsqlRange[] multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); - - /// - /// Determines whether a multirange does not extend to the left of a range. - /// - /// The multirange. - /// The multirange. - /// - /// true - /// if the multirange does not extend to the left of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of - /// an EF Core LINQ query. - /// - public static bool DoesNotExtendLeftOf(this List> multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); - - #endregion DoesNotExtendLeftOf - - #region DoesNotExtendRightOf - - /// - /// Determines whether a multirange does not extend to the right of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange does not extend to the right of the multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of - /// an EF Core LINQ query. - /// - public static bool DoesNotExtendRightOf(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); - - /// - /// Determines whether a multirange does not extend to the right of another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the first multirange does not extend to the right of the multirange; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as - /// part of an EF Core LINQ query. - /// - public static bool DoesNotExtendRightOf(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); - - /// - /// Determines whether a multirange does not extend to the right of a range. - /// - /// The multirange. - /// The multirange. - /// - /// true - /// if the multirange does not extend to the right of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static bool DoesNotExtendRightOf(this NpgsqlRange[] multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); - - /// - /// Determines whether a multirange does not extend to the right of a range. - /// - /// The multirange. - /// The multirange. - /// - /// true - /// if the multirange does not extend to the right of the range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of - /// an EF Core LINQ query. - /// - public static bool DoesNotExtendRightOf(this List> multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); - - #endregion DoesNotExtendRightOf - - #region IsAdjacentTo - - /// - /// Determines whether a multirange is adjacent to another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the multiranges are adjacent; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool IsAdjacentTo(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); - - /// - /// Determines whether a multirange is adjacent to another multirange. - /// - /// The first multirange. - /// The second multirange. - /// - /// true - /// if the multiranges are adjacent; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of - /// an EF Core LINQ query. - /// - public static bool IsAdjacentTo(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); - - /// - /// Determines whether a multirange is adjacent to a range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange and range are adjacent; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static bool IsAdjacentTo(this NpgsqlRange[] multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); - - /// - /// Determines whether a multirange is adjacent to a range. - /// - /// The multirange. - /// The range. - /// - /// true - /// if the multirange and range are adjacent; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static bool IsAdjacentTo(this List> multirange, NpgsqlRange range) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); - - #endregion IsAdjacentTo - - #region Union - - /// - /// Returns the set union, which means unique elements that appear in either of two multiranges. - /// - /// The first multirange. - /// The second multirange. - /// A multirange containing the unique elements that appear in either multirange. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static NpgsqlRange[] Union(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); - - /// - /// Returns the set union, which means unique elements that appear in either of two multiranges. - /// - /// The first multirange. - /// The second multirange. - /// A multirange containing the unique elements that appear in either multirange. - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static List> Union(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); - - #endregion Union - - #region Intersect - - /// - /// Returns the set intersection, which means elements that appear in each of two multiranges. - /// - /// The first multirange. - /// The second multirange. - /// A multirange containing the elements that appear in both ranges. - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static NpgsqlRange[] Intersect(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); - - /// - /// Returns the set intersection, which means elements that appear in each of two multiranges. - /// - /// The first multirange. - /// The second multirange. - /// A multirange containing the elements that appear in both ranges. - /// - /// is only intended for use via SQL translation as part of an - /// EF Core LINQ query. - /// - public static List> Intersect(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); - - #endregion Intersect - - #region Except - - /// - /// Returns the set difference, which means the elements of one multirange that do not appear in a second multirange. - /// - /// The first multirange. - /// The second multirange. - /// A multirange containing the elements that appear in the first range, but not the second range. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static NpgsqlRange[] Except(this NpgsqlRange[] multirange1, NpgsqlRange[] multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Except))); - - /// - /// Returns the set difference, which means the elements of one multirange that do not appear in a second multirange. - /// - /// The first multirange. - /// The second multirange. - /// A multirange containing the elements that appear in the first range, but not the second range. - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static List> Except(this List> multirange1, List> multirange2) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Except))); - - #endregion Except - - #region Merge - - /// - /// Computes the smallest range that includes the entire multirange. - /// - /// The multirange. - /// The smallest range that includes the entire multirange. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlRange Merge(this NpgsqlRange[] multirange) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); - - /// - /// Computes the smallest range that includes the entire multirange. - /// - /// The multirange. - /// The smallest range that includes the entire multirange. - /// - /// is only intended for use via SQL translation as part of an EF - /// Core LINQ query. - /// - public static NpgsqlRange Merge(this List> multirange) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); - - #endregion Merge -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlNetworkDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlNetworkDbFunctionsExtensions.cs deleted file mode 100644 index c9eb4a6885..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlNetworkDbFunctionsExtensions.cs +++ /dev/null @@ -1,1164 +0,0 @@ -using System.Net; -using System.Net.NetworkInformation; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods supporting operator translation for PostgreSQL network types. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-net.html -/// -public static class NpgsqlNetworkDbFunctionsExtensions -{ - #region RelationalOperators - - /// - /// Determines whether an is less than another . - /// - /// The instance. - /// The left-hand inet. - /// The right-hand inet. - /// - /// True if the is less than the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool LessThan(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan))); - - /// - /// Determines whether an is less than another . - /// - /// The instance. - /// The left-hand macaddr. - /// The right-hand macaddr. - /// - /// True if the is less than the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool LessThan(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThan))); - - /// - /// Determines whether an is less than or equal to another . - /// - /// The instance. - /// The left-hand inet. - /// The right-hand inet. - /// - /// True if the is less than or equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool LessThanOrEqual(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual))); - - /// - /// Determines whether an is less than or equal to another . - /// - /// The instance. - /// The left-hand macaddr. - /// The right-hand macaddr. - /// - /// True if the is less than or equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool LessThanOrEqual(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(LessThanOrEqual))); - - /// - /// Determines whether an is greater than or equal to another . - /// - /// The instance. - /// The left-hand inet. - /// The right-hand inet. - /// - /// True if the is greater than or equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool GreaterThanOrEqual(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual))); - - /// - /// Determines whether an is greater than or equal to another . - /// - /// The instance. - /// The left-hand macaddr. - /// The right-hand macaddr. - /// - /// True if the is greater than or equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool GreaterThanOrEqual(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThanOrEqual))); - - /// - /// Determines whether an is greater than another . - /// - /// The instance. - /// The left-hand inet. - /// The right-hand inet. - /// - /// True if the is greater than the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool GreaterThan(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan))); - - /// - /// Determines whether an is greater than another . - /// - /// The instance. - /// The left-hand macaddr. - /// The right-hand macaddr. - /// - /// True if the is greater than the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool GreaterThan(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(GreaterThan))); - - #endregion - - #region ContainmentOperators - - /// - /// Determines whether an is contained within another . - /// - /// The instance. - /// The inet to locate. - /// The inet to search. - /// - /// True if the is contained within the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool ContainedBy(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - /// - /// Determines whether an is contained within or equal to another . - /// - /// The instance. - /// The inet to locate. - /// The inet to search. - /// - /// True if the is contained within or equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool ContainedByOrEqual(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedByOrEqual))); - - /// - /// Determines whether an contains another . - /// - /// The instance. - /// The IP address to search. - /// The IP address to locate. - /// - /// True if the contains the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Contains(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether an contains or is equal to another . - /// - /// The instance. - /// The IP address to search. - /// The IP address to locate. - /// - /// True if the contains or is equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool ContainsOrEqual(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsOrEqual))); - - /// - /// Determines whether an contains or is contained by another . - /// - /// The instance. - /// The IP address to search. - /// The IP address to locate. - /// - /// True if the contains or is contained by the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool ContainsOrContainedBy(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsOrContainedBy))); - - #endregion - - #region BitwiseOperators - - /// - /// Computes the bitwise NOT operation on an . - /// - /// The instance. - /// The inet to negate. - /// - /// The result of the bitwise NOT operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet BitwiseNot(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseNot))); - - /// - /// Computes the bitwise NOT operation on an . - /// - /// The instance. - /// The macaddr to negate. - /// - /// The result of the bitwise NOT operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static PhysicalAddress BitwiseNot(this DbFunctions _, PhysicalAddress macaddr) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseNot))); - - /// - /// Computes the bitwise AND of two instances. - /// - /// The instance. - /// The left-hand inet. - /// The right-hand inet. - /// - /// The result of the bitwise AND operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet BitwiseAnd(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseAnd))); - - /// - /// Computes the bitwise AND of two instances. - /// - /// The instance. - /// The left-hand macaddr. - /// The right-hand macaddr. - /// - /// The result of the bitwise AND operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static PhysicalAddress BitwiseAnd(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseAnd))); - - /// - /// Computes the bitwise OR of two instances. - /// - /// The instance. - /// The left-hand inet. - /// The right-hand inet. - /// - /// The result of the bitwise OR operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet BitwiseOr(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseOr))); - - /// - /// Computes the bitwise OR of two instances. - /// - /// The instance. - /// The left-hand macaddr. - /// The right-hand macaddr. - /// - /// The result of the bitwise OR operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static PhysicalAddress BitwiseOr(this DbFunctions _, PhysicalAddress macaddr, PhysicalAddress other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(BitwiseOr))); - - #endregion - - #region ArithmeticOperators - - /// - /// Adds the to the . - /// - /// The instance. - /// The inet. - /// The value to add. - /// - /// The augmented by the . - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet Add(this DbFunctions _, NpgsqlInet inet, int value) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Add))); - - /// - /// Subtracts the from the . - /// - /// The instance. - /// The inet. - /// The value to subtract. - /// - /// The augmented by the . - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet Subtract(this DbFunctions _, NpgsqlInet inet, long value) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Subtract))); - - /// - /// Subtracts one from another . - /// - /// The instance. - /// The inet from which to subtract. - /// The inet to subtract. - /// - /// The numeric difference between the two given addresses. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static int Subtract(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Subtract))); - - #endregion - - #region Functions - - /// - /// Returns the abbreviated display format as text. - /// - /// The instance. - /// The inet to abbreviate. - /// - /// The abbreviated display format as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static string Abbreviate(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Abbreviate))); - - /// - /// Returns the abbreviated display format as text. - /// - /// The instance. - /// The cidr to abbreviate. - /// - /// The abbreviated display format as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static string Abbreviate(this DbFunctions _, IPNetwork cidr) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Abbreviate))); - -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - /// - /// Returns the abbreviated display format as text. - /// - /// The instance. - /// The cidr to abbreviate. - /// - /// The abbreviated display format as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static string Abbreviate(this DbFunctions _, NpgsqlCidr cidr) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Abbreviate))); -#pragma warning restore CS0618 - - /// - /// Returns the broadcast address for a network. - /// - /// The instance. - /// The inet used to derive the broadcast address. - /// - /// The broadcast address for a network. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet Broadcast(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Broadcast))); - - /// - /// Extracts the family of an address; 4 for IPv4, 6 for IPv6. - /// - /// The instance. - /// The inet used to derive the family. - /// - /// The family of an address; 4 for IPv4, 6 for IPv6. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static int Family(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Family))); - - /// - /// Extracts the host (i.e. the IP address) as text. - /// - /// The instance. - /// The inet from which to extract the host. - /// - /// The host (i.e. the IP address) as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static string Host(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Host))); - - /// - /// Constructs the host mask for the network. - /// - /// The instance. - /// The inet used to construct the host mask. - /// - /// The constructed host mask. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet HostMask(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(HostMask))); - - /// - /// Extracts the length of the subnet mask. - /// - /// The instance. - /// The inet used to extract the subnet length. - /// - /// The length of the subnet mask. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static int MaskLength(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(MaskLength))); - - /// - /// Constructs the subnet mask for the network. - /// - /// The instance. - /// The inet used to construct the subnet mask. - /// - /// The subnet mask for the network. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet Netmask(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Netmask))); - - /// - /// Extracts the network part of the address. - /// - /// The instance. - /// The inet used to extract the network. - /// - /// The network part of the address. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static IPNetwork Network(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Network))); - - /// - /// Sets the length of the subnet mask. - /// - /// The instance. - /// The inet to modify. - /// The subnet mask length to set. - /// - /// The network with a subnet mask of the specified length. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlInet SetMaskLength(this DbFunctions _, NpgsqlInet inet, int length) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SetMaskLength))); - - /// - /// Sets the length of the subnet mask. - /// - /// The instance. - /// The cidr to modify. - /// The subnet mask length to set. - /// - /// The network with a subnet mask of the specified length. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static IPNetwork SetMaskLength(this DbFunctions _, IPNetwork cidr, int length) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SetMaskLength))); - -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - /// - /// Sets the length of the subnet mask. - /// - /// The instance. - /// The cidr to modify. - /// The subnet mask length to set. - /// - /// The network with a subnet mask of the specified length. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlCidr SetMaskLength(this DbFunctions _, NpgsqlCidr cidr, int length) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SetMaskLength))); -#pragma warning restore CS0618 - - /// - /// Extracts the IP address and subnet mask as text. - /// - /// The instance. - /// The inet to extract as text. - /// - /// The IP address and subnet mask as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static string Text(this DbFunctions _, NpgsqlInet inet) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Text))); - - /// - /// Tests if the addresses are in the same family. - /// - /// The instance. - /// The primary inet. - /// The other inet. - /// - /// True if the addresses are in the same family; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool SameFamily(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SameFamily))); - - /// - /// Constructs the smallest network which includes both of the given networks. - /// - /// The instance. - /// The first inet. - /// The second inet. - /// - /// The smallest network which includes both of the given networks. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static IPNetwork Merge(this DbFunctions _, NpgsqlInet inet, NpgsqlInet other) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); - - /// - /// Sets the last 3 bytes of the MAC address to zero. For macaddr8, the last 5 bytes are set to zero. - /// - /// The instance. - /// The MAC address to truncate. - /// - /// The MAC address with the last 3 bytes set to zero. For macaddr8, the last 5 bytes are set to zero. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static PhysicalAddress Truncate(this DbFunctions _, PhysicalAddress macAddress) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Truncate))); - - /// - /// Sets the 7th bit to one, also known as modified EUI-64, for inclusion in an IPv6 address. - /// - /// The instance. - /// The MAC address to modify. - /// - /// The MAC address with the 7th bit set to one. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static PhysicalAddress Set7BitMac8(this DbFunctions _, PhysicalAddress macAddress) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Set7BitMac8))); - - #endregion - - #region Obsolete - - /// - /// Determines whether an (IPAddress Address, int Subnet) is less than another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The left-hand cidr. - /// The right-hand cidr. - /// - /// True if the (IPAddress Address, int Subnet) is less than the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool LessThan(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) is less than or equal to another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The left-hand cidr. - /// The right-hand cidr. - /// - /// True if the (IPAddress Address, int Subnet) is less than or equal to the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool LessThanOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) is greater than or equal to another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The left-hand cidr. - /// The right-hand cidr. - /// - /// True if the (IPAddress Address, int Subnet) is greater than or equal to the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool GreaterThanOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) is greater than another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The left-hand cidr. - /// The right-hand cidr. - /// - /// True if the (IPAddress Address, int Subnet) is greater than the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool GreaterThan(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an is contained within a network. - /// - /// The instance. - /// The inet to locate. - /// The cidr to search. - /// - /// True if the is contained within the network; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainedBy(this DbFunctions _, IPAddress inet, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether a network contains or is equal to another . - /// - /// The instance. - /// The network to search. - /// The IP address to locate. - /// - /// True if the network contains or is equal to the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainsOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, IPAddress other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) is contained within another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr to locate. - /// The cidr to search. - /// - /// True if the (IPAddress Address, int Subnet) is contained within the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainedBy(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an is contained within or equal to a network. - /// - /// The instance. - /// The inet to locate. - /// The cidr to search. - /// - /// True if the is contained within or equal to the network; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainedByOrEqual(this DbFunctions _, IPAddress inet, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) is contained within or equal to another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr to locate. - /// The cidr to search. - /// - /// True if the (IPAddress Address, int Subnet) is contained within or equal to the other (IPAddress Address, int Subnet); otherwise, - /// false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainedByOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether a network contains another . - /// - /// The instance. - /// The network to search. - /// The IP address to locate. - /// - /// True if the network contains the other ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool Contains( - this DbFunctions _, - (IPAddress Address, int Subnet) cidr, - IPAddress other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) contains another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr to search. - /// The cidr to locate. - /// - /// True if the (IPAddress Address, int Subnet) contains the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool Contains(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) contains or is equal to another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr to search. - /// The cidr to locate. - /// - /// True if the (IPAddress Address, int Subnet) contains or is equal to the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainsOrEqual(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether a network contains or is contained by an . - /// - /// The instance. - /// The network to search. - /// The IP address to locate. - /// - /// True if the network contains or is contained by the ; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainsOrContainedBy(this DbFunctions _, (IPAddress Address, int Subnet) cidr, IPAddress other) - => throw new NotSupportedException(); - - /// - /// Determines whether an contains or is contained by a network. - /// - /// The instance. - /// The IP address to search. - /// The network to locate. - /// - /// True if the contains or is contained by the network; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainsOrContainedBy(this DbFunctions _, IPAddress inet, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Determines whether an (IPAddress Address, int Subnet) contains or is contained by another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr to search. - /// The cidr to locate. - /// - /// True if the (IPAddress Address, int Subnet) contains or is contained by the other (IPAddress Address, int Subnet); otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool ContainsOrContainedBy( - this DbFunctions _, - (IPAddress Address, int Subnet) cidr, - (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Computes the bitwise NOT operation on an (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr to negate. - /// - /// The result of the bitwise NOT operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) BitwiseNot(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Computes the bitwise AND of two (IPAddress Address, int Subnet) instances. - /// - /// The instance. - /// The left-hand cidr. - /// The right-hand cidr. - /// - /// The result of the bitwise AND operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) BitwiseAnd( - this DbFunctions _, - (IPAddress Address, int Subnet) cidr, - (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Computes the bitwise OR of two (IPAddress Address, int Subnet) instances. - /// - /// The instance. - /// The left-hand cidr. - /// The right-hand cidr. - /// - /// The result of the bitwise OR operation. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) BitwiseOr( - this DbFunctions _, - (IPAddress Address, int Subnet) cidr, - (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Adds the to the (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr. - /// The value to add. - /// - /// The (IPAddress Address, int Subnet) augmented by the . - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) Add(this DbFunctions _, (IPAddress Address, int Subnet) cidr, int value) - => throw new NotSupportedException(); - - /// - /// Subtracts the from the (IPAddress Address, int Subnet). - /// - /// The instance. - /// The inet. - /// The value to subtract. - /// - /// The (IPAddress Address, int Subnet) augmented by the . - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) Subtract(this DbFunctions _, (IPAddress Address, int Subnet) cidr, int value) - => throw new NotSupportedException(); - - /// - /// Subtracts one (IPAddress Address, int Subnet) from another (IPAddress Address, int Subnet). - /// - /// The instance. - /// The cidr from which to subtract. - /// The cidr to subtract. - /// - /// The difference between the two addresses. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static int Subtract( - this DbFunctions _, - (IPAddress Address, int Subnet) cidr, - (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Returns the abbreviated display format as text. - /// - /// The instance. - /// The cidr to abbreviate. - /// - /// The abbreviated display format as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static string Abbreviate(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Returns the broadcast address for a network. - /// - /// The instance. - /// The cidr used to derive the broadcast address. - /// - /// The broadcast address for a network. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static IPAddress Broadcast(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Extracts the family of an address; 4 for IPv4, 6 for IPv6. - /// - /// The instance. - /// The cidr used to derive the family. - /// - /// The family of an address; 4 for IPv4, 6 for IPv6. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static int Family(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Extracts the host (i.e. the IP address) as text. - /// - /// The instance. - /// The cidr from which to extract the host. - /// - /// The host (i.e. the IP address) as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static string Host(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Constructs the host mask for the network. - /// - /// The instance. - /// The cidr used to construct the host mask. - /// - /// The constructed host mask. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static IPAddress HostMask(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Extracts the length of the subnet mask. - /// - /// The instance. - /// The cidr used to extract the subnet length. - /// - /// The length of the subnet mask. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static int MaskLength(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Constructs the subnet mask for the network. - /// - /// The instance. - /// The cidr used to construct the subnet mask. - /// - /// The subnet mask for the network. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static IPAddress Netmask(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Extracts the network part of the address. - /// - /// The instance. - /// The cidr used to extract the network. - /// - /// The network part of the address. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) Network(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Sets the length of the subnet mask. - /// - /// The instance. - /// The cidr to modify. - /// The subnet mask length to set. - /// - /// The network with a subnet mask of the specified length. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) SetMaskLength(this DbFunctions _, (IPAddress Address, int Subnet) cidr, int length) - => throw new NotSupportedException(); - - /// - /// Extracts the IP address and subnet mask as text. - /// - /// The instance. - /// The cidr to extract as text. - /// - /// The IP address and subnet mask as text. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static string Text(this DbFunctions _, (IPAddress Address, int Subnet) cidr) - => throw new NotSupportedException(); - - /// - /// Tests if the addresses are in the same family. - /// - /// The instance. - /// The primary cidr. - /// The other cidr. - /// - /// True if the addresses are in the same family; otherwise, false. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static bool SameFamily(this DbFunctions _, (IPAddress Address, int Subnet) cidr, (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - /// - /// Constructs the smallest network which includes both of the given networks. - /// - /// The instance. - /// The first cidr. - /// The second cidr. - /// - /// The smallest network which includes both of the given networks. - /// - /// - /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - [Obsolete("Use the overload which accepts NpgsqlCidr", error: true)] - public static (IPAddress Address, int Subnet) Merge( - this DbFunctions _, - (IPAddress Address, int Subnet) cidr, - (IPAddress Address, int Subnet) other) - => throw new NotSupportedException(); - - #endregion Obsolete -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlRangeDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlRangeDbFunctionsExtensions.cs deleted file mode 100644 index 269260d17e..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlRangeDbFunctionsExtensions.cs +++ /dev/null @@ -1,264 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods for supporting PostgreSQL translation. -/// -public static class NpgsqlRangeDbFunctionsExtensions -{ - /// - /// Determines whether a range contains a specified value. - /// - /// The range in which to locate the value. - /// The value to locate in the range. - /// The type of the elements of . - /// - /// true - /// if the range contains the specified value; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Contains(this NpgsqlRange range, T value) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a range contains a specified range. - /// - /// The range in which to locate the specified range. - /// The specified range to locate in the range. - /// The type of the elements of . - /// - /// true - /// if the range contains the specified range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ - /// query. - /// - public static bool Contains(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); - - /// - /// Determines whether a range is contained by a specified range. - /// - /// The specified range to locate in the range. - /// The range in which to locate the specified range. - /// The type of the elements of . - /// - /// true - /// if the range contains the specified range; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool ContainedBy(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainedBy))); - - /// - /// Determines whether a range overlaps another range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// true - /// if the ranges overlap (share points in common); otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool Overlaps(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Overlaps))); - - /// - /// Determines whether a range is strictly to the left of another range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// true - /// if the first range is strictly to the left of the second; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool IsStrictlyLeftOf(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyLeftOf))); - - /// - /// Determines whether a range is strictly to the right of another range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// true - /// if the first range is strictly to the right of the second; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool IsStrictlyRightOf(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsStrictlyRightOf))); - - /// - /// Determines whether a range does not extend to the left of another range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// true - /// if the first range does not extend to the left of the second; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool DoesNotExtendLeftOf(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendLeftOf))); - - /// - /// Determines whether a range does not extend to the right of another range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// true - /// if the first range does not extend to the right of the second; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool DoesNotExtendRightOf(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DoesNotExtendRightOf))); - - /// - /// Determines whether a range is adjacent to another range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// true - /// if the ranges are adjacent; otherwise, - /// false - /// . - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static bool IsAdjacentTo(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsAdjacentTo))); - - /// - /// Returns the set union, which means unique elements that appear in either of two ranges. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// A range containing the unique elements that appear in either range. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlRange Union(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Union))); - - /// - /// Returns the set intersection, which means elements that appear in each of two ranges. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// A range containing the elements that appear in both ranges. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlRange Intersect(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Intersect))); - - /// - /// Returns the set difference, which means the elements of one range that do not appear in a second range. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// The elements that appear in the first range, but not the second range. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlRange Except(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Except))); - - /// - /// Returns the smallest range which includes both of the given ranges. - /// - /// The first range. - /// The second range. - /// The type of the elements of . - /// - /// The smallest range which includes both of the given ranges. - /// - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlRange Merge(this NpgsqlRange a, NpgsqlRange b) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Merge))); - - /// - /// Computes the union of the non-null input ranges. Corresponds to the PostgreSQL range_agg aggregate function. - /// - /// The ranges to be aggregated via union into a multirange. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core LINQ query. - /// - public static NpgsqlRange[] RangeAgg(this IEnumerable> input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeAgg))); - - /// - /// Computes the intersection of the non-null input ranges. Corresponds to the PostgreSQL range_intersect_agg aggregate function. - /// - /// The ranges on which to perform the intersection operation. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static NpgsqlRange RangeIntersectAgg(this IEnumerable> input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); - - /// - /// Computes the intersection of the non-null input multiranges. - /// Corresponds to the PostgreSQL range_intersect_agg aggregate function. - /// - /// The multiranges on which to perform the intersection operation. - /// PostgreSQL documentation for aggregate functions. - /// - /// is only intended for use via SQL translation as part of an EF Core - /// LINQ query. - /// - public static NpgsqlRange[] RangeIntersectAgg(this IEnumerable[]> input) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(RangeIntersectAgg))); -} diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlTrigramsDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlTrigramsDbFunctionsExtensions.cs deleted file mode 100644 index 79710d10a4..0000000000 --- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlTrigramsDbFunctionsExtensions.cs +++ /dev/null @@ -1,166 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. -/// The methods on this class are accessed via . -/// -/// -/// See Database functions. -/// -public static class NpgsqlTrigramsDbFunctionsExtensions -{ - /// - /// Returns an array of all the trigrams in the given . - /// (In practice this is seldom useful except for debugging.) - /// - /// - /// The method call is translated to show_trgm(text). - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static string[] TrigramsShow(this DbFunctions _, string text) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsShow))); - - /// - /// Returns a number that indicates how similar the two arguments are. - /// The range of the result is zero (indicating that the two strings are - /// completely dissimilar) to one (indicating that the two strings are identical). - /// - /// - /// The method call is translated to similarity(source, target). - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsSimilarity(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsSimilarity))); - - /// - /// Returns a number that indicates the greatest similarity between the set of trigrams - /// in the first string and any continuous extent of an ordered set of trigrams - /// in the second string. - /// - /// - /// The method call is translated to word_similarity(source, target). - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsWordSimilarity(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsWordSimilarity))); - - /// - /// Same as word_similarity(text, text), but forces extent boundaries to match word boundaries. - /// Since we don't have cross-word trigrams, this function actually returns greatest similarity - /// between first string and any continuous extent of words of the second string. - /// - /// - /// The method call is translated to strict_word_similarity(source, target). - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsStrictWordSimilarity(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsStrictWordSimilarity))); - - /// - /// Returns true if its arguments have a similarity that is greater than the current similarity - /// threshold set by pg_trgm.similarity_threshold. - /// - /// - /// The method call is translated to source % target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static bool TrigramsAreSimilar(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreSimilar))); - - /// - /// Returns true if the similarity between the trigram set in the first argument and a continuous - /// extent of an ordered trigram set in the second argument is greater than the current word similarity - /// threshold set by pg_trgm.word_similarity_threshold parameter. - /// - /// - /// The method call is translated to source <% target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static bool TrigramsAreWordSimilar(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreWordSimilar))); - - /// - /// Commutator of the <% operator. - /// - /// - /// The method call is translated to source %> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static bool TrigramsAreNotWordSimilar(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreNotWordSimilar))); - - /// - /// Returns true if its second argument has a continuous extent of an ordered trigram set that - /// matches word boundaries, and its similarity to the trigram set of the first argument is greater - /// than the current strict word similarity threshold set by the pg_trgm.strict_word_similarity_threshold - /// parameter. - /// - /// - /// The method call is translated to source <<% target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static bool TrigramsAreStrictWordSimilar(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreStrictWordSimilar))); - - /// - /// Commutator of the <<% operator. - /// - /// - /// The method call is translated to source %>> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static bool TrigramsAreNotStrictWordSimilar(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsAreNotStrictWordSimilar))); - - /// - /// Returns the "distance" between the arguments, that is one minus the similarity() value. - /// - /// - /// The method call is translated to source <-> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsSimilarityDistance(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsSimilarityDistance))); - - /// - /// Returns the "distance" between the arguments, that is one minus the word_similarity() value. - /// - /// - /// The method call is translated to source <<-> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsWordSimilarityDistance(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsWordSimilarityDistance))); - - /// - /// Commutator of the <<-> operator. - /// - /// - /// The method call is translated to source <->> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsWordSimilarityDistanceInverted(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsWordSimilarityDistanceInverted))); - - /// - /// Returns the "distance" between the arguments, that is one minus the strict_word_similarity() value. - /// - /// - /// The method call is translated to source <<<-> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsStrictWordSimilarityDistance(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsStrictWordSimilarityDistance))); - - /// - /// Commutator of the <<<-> operator. - /// - /// - /// The method call is translated to source <->>> target. - /// See https://www.postgresql.org/docs/current/pgtrgm.html. - /// - public static double TrigramsStrictWordSimilarityDistanceInverted(this DbFunctions _, string source, string target) - => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TrigramsStrictWordSimilarityDistanceInverted))); -} diff --git a/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs b/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs deleted file mode 100644 index 4ccbbe20b9..0000000000 --- a/src/EFCore.PG/Extensions/Internal/NpgsqlShapedQueryExpressionExtensions.cs +++ /dev/null @@ -1,245 +0,0 @@ -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 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 JSON-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 TryExtractJsonArray( - this ShapedQueryExpression source, - [NotNullWhen(true)] out SqlExpression? jsonArray, - [NotNullWhen(true)] out SqlExpression? projectedElement, - out bool isElementNullable, - bool ignoreOrderings = false, - bool ignorePredicate = false) - { - if (source.QueryExpression is SelectExpression - { - Tables: - [ - TableValuedFunctionExpression - { - Name: "jsonb_array_elements_text" or "json_array_elements_text", - Arguments: [var json] - } tvf - ], - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - } select - && (ignorePredicate || select.Predicate is null) - // We can only apply the indexing if the 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 == tvf.Alias)) - && TryGetScalarProjection(source, out var projectedScalar)) - { - jsonArray = json; - - // The projected ColumnExpression is wrapped in a Convert to apply the element type mapping - unless it happens to be text. - switch (projectedScalar) - { - case SqlUnaryExpression - { - OperatorType: ExpressionType.Convert, - Operand: ColumnExpression { IsNullable: var isNullable } - } convert: - projectedElement = convert; - isElementNullable = isNullable; - return true; - case ColumnExpression { IsNullable: var isNullable } column: - projectedElement = column; - isElementNullable = isNullable; - return true; - default: - throw new UnreachableException(); - } - } - - jsonArray = null; - projectedElement = null; - isElementNullable = false; - 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 TryConvertToArray( - 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, - { TypeMapping: NpgsqlJsonTypeMapping } => false, - { Type: var type } when type.IsMultirange() => false, - _ => true - }; - - private static bool TryGetProjectedColumn( - ShapedQueryExpression shapedQueryExpression, - [NotNullWhen(true)] out ColumnExpression? projectedColumn) - { - if (TryGetScalarProjection(shapedQueryExpression, out var scalar) && scalar is ColumnExpression column) - { - projectedColumn = column; - return true; - } - - projectedColumn = null; - return false; - } - - private static bool TryGetScalarProjection( - ShapedQueryExpression shapedQueryExpression, - [NotNullWhen(true)] out SqlExpression? projectedScalar) - { - 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 SqlExpression scalar) - { - projectedScalar = scalar; - return true; - } - - projectedScalar = null; - return false; - } -} diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlEntityTypeExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlEntityTypeExtensions.cs deleted file mode 100644 index 181090e86c..0000000000 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlEntityTypeExtensions.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Extension methods for for Npgsql-specific metadata. -/// -public static class NpgsqlEntityTypeExtensions -{ - #region Storage parameters - - /// - /// Gets all storage parameters for the table mapped to the entity type. - /// - public static Dictionary GetStorageParameters(this IReadOnlyEntityType entityType) - => entityType.GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) - .ToDictionary( - a => a.Name.Substring(NpgsqlAnnotationNames.StorageParameterPrefix.Length), - a => a.Value); - - /// - /// Gets a storage parameter for the table mapped to the entity type. - /// - public static string? GetStorageParameter(this IEntityType entityType, string parameterName) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - return (string?)entityType[NpgsqlAnnotationNames.StorageParameterPrefix + parameterName]; - } - - /// - /// Sets a storage parameter on the table mapped to the entity type. - /// - public static void SetStorageParameter(this IMutableEntityType entityType, string parameterName, object? parameterValue) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - entityType.SetOrRemoveAnnotation(NpgsqlAnnotationNames.StorageParameterPrefix + parameterName, parameterValue); - } - - /// - /// Sets a storage parameter on the table mapped to the entity type. - /// - public static object SetStorageParameter( - this IConventionEntityType entityType, - string parameterName, - object? parameterValue, - bool fromDataAnnotation = false) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - entityType.SetOrRemoveAnnotation(NpgsqlAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); - - return parameterName; - } - - /// - /// Gets the configuration source for a storage parameter for the table mapped to the entity type. - /// - public static ConfigurationSource? GetStorageParameterConfigurationSource( - this IConventionEntityType index, - string parameterName) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - return index.FindAnnotation(NpgsqlAnnotationNames.StorageParameterPrefix + parameterName)?.GetConfigurationSource(); - } - - #endregion Storage parameters - - #region Unlogged - - /// - /// Gets whether the table to which the entity is mapped is unlogged. - /// - public static bool GetIsUnlogged(this IReadOnlyEntityType entityType) - => entityType[NpgsqlAnnotationNames.UnloggedTable] as bool? ?? false; - - /// - /// Sets whether the table to which the entity is mapped is unlogged. - /// - public static void SetIsUnlogged(this IMutableEntityType entityType, bool unlogged) - => entityType.SetOrRemoveAnnotation(NpgsqlAnnotationNames.UnloggedTable, unlogged); - - /// - /// Sets whether the table to which the entity is mapped is unlogged. - /// - public static bool SetIsUnlogged( - this IConventionEntityType entityType, - bool unlogged, - bool fromDataAnnotation = false) - { - entityType.SetOrRemoveAnnotation(NpgsqlAnnotationNames.UnloggedTable, unlogged, fromDataAnnotation); - - return unlogged; - } - - /// - /// Gets the configuration source for whether the table to which the entity is mapped is unlogged. - /// - public static ConfigurationSource? GetIsUnloggedConfigurationSource(this IConventionEntityType index) - => index.FindAnnotation(NpgsqlAnnotationNames.UnloggedTable)?.GetConfigurationSource(); - - #endregion Unlogged - - #region CockroachDb interleave in parent - - /// - /// Gets the CockroachDB-specific interleave-in-parent setting for the table to which the entity is mapped. - /// - public static CockroachDbInterleaveInParent GetCockroachDbInterleaveInParent(this IReadOnlyEntityType entityType) - => new(entityType); - - #endregion CockroachDb interleave in parent -} diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs deleted file mode 100644 index e3e86d6bbd..0000000000 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlIndexExtensions.cs +++ /dev/null @@ -1,482 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Extension methods for for Npgsql-specific metadata. -/// -public static class NpgsqlIndexExtensions -{ - #region Method - - /// - /// Returns the index method to be used, or null if it hasn't been specified. - /// null selects the default (currently btree). - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - public static string? GetMethod(this IReadOnlyIndex index) - => (string?)index[NpgsqlAnnotationNames.IndexMethod]; - - /// - /// Sets the index method to be used, or null if it hasn't been specified. - /// null selects the default (currently btree). - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - public static void SetMethod(this IMutableIndex index, string? method) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexMethod, method); - - /// - /// Sets the index method to be used, or null if it hasn't been specified. - /// null selects the default (currently btree). - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - public static string? SetMethod( - this IConventionIndex index, - string? method, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(method, nameof(method)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexMethod, method, fromDataAnnotation); - - return method; - } - - /// - /// Returns the for the index method. - /// - /// The index. - /// The for the index method. - public static ConfigurationSource? GetMethodConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.IndexMethod)?.GetConfigurationSource(); - - #endregion Method - - #region Operators - - /// - /// Returns the column operators to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-opclass.html - /// - public static IReadOnlyList? GetOperators(this IReadOnlyIndex index) - => (IReadOnlyList?)index[NpgsqlAnnotationNames.IndexOperators]; - - /// - /// Sets the column operators to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-opclass.html - /// - public static void SetOperators(this IMutableIndex index, IReadOnlyList? operators) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexOperators, operators); - - /// - /// Sets the column operators to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-opclass.html - /// - public static IReadOnlyList? SetOperators( - this IConventionIndex index, - IReadOnlyList? operators, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(operators, nameof(operators)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexOperators, operators, fromDataAnnotation); - - return operators; - } - - /// - /// Returns the for the index operators. - /// - /// The index. - /// The for the index operators. - public static ConfigurationSource? GetOperatorsConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.IndexOperators)?.GetConfigurationSource(); - - #endregion Operators - - #region Collation - - /// - /// Returns the column collations to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// -#pragma warning disable 618 - public static IReadOnlyList? GetCollation(this IReadOnlyIndex index) - => (IReadOnlyList?)( - index[RelationalAnnotationNames.Collation] ?? index[NpgsqlAnnotationNames.IndexCollation]); -#pragma warning restore 618 - - /// - /// Sets the column collations to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// - public static void SetCollation(this IMutableIndex index, IReadOnlyList? collations) - => index.SetOrRemoveAnnotation(RelationalAnnotationNames.Collation, collations); - - /// - /// Sets the column collations to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-collations.html - /// - public static IReadOnlyList? SetCollation( - this IConventionIndex index, - IReadOnlyList? collations, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(collations, nameof(collations)); - - index.SetOrRemoveAnnotation(RelationalAnnotationNames.Collation, collations, fromDataAnnotation); - - return collations; - } - - /// - /// Returns the for the index collations. - /// - /// The index. - /// The for the index collations. - public static ConfigurationSource? GetCollationConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource(); - - #endregion Collation - - #region Null sort order - - /// - /// Returns the column NULL sort orders to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - public static IReadOnlyList? GetNullSortOrder(this IReadOnlyIndex index) - => (IReadOnlyList?)index[NpgsqlAnnotationNames.IndexNullSortOrder]; - - /// - /// Sets the column NULL sort orders to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - public static void SetNullSortOrder(this IMutableIndex index, IReadOnlyList? nullSortOrder) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexNullSortOrder, nullSortOrder); - - /// - /// Sets the column NULL sort orders to be used, or null if they have not been specified. - /// - /// - /// https://www.postgresql.org/docs/current/static/indexes-ordering.html - /// - public static IReadOnlyList? SetNullSortOrder( - this IConventionIndex index, - IReadOnlyList? nullSortOrder, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(nullSortOrder, nameof(nullSortOrder)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IndexNullSortOrder, nullSortOrder, fromDataAnnotation); - - return nullSortOrder; - } - - /// - /// Returns the for the index null sort orders. - /// - /// The index. - /// The for the index null sort orders. - public static ConfigurationSource? GetNullSortOrderConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.IndexNullSortOrder)?.GetConfigurationSource(); - - #endregion - - #region Included properties - - /// - /// Returns included property names, or null if they have not been specified. - /// - /// The index. - /// The included property names, or null if they have not been specified. - public static IReadOnlyList? GetIncludeProperties(this IReadOnlyIndex index) - => (IReadOnlyList?)index[NpgsqlAnnotationNames.IndexInclude]; - - /// - /// Sets included property names. - /// - /// The index. - /// The value to set. - public static void SetIncludeProperties(this IMutableIndex index, IReadOnlyList? properties) - => index.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.IndexInclude, - properties); - - /// - /// Sets included property names. - /// - /// The index. - /// Indicates whether the configuration was specified using a data annotation. - /// The value to set. - public static IReadOnlyList? SetIncludeProperties( - this IConventionIndex index, - IReadOnlyList? properties, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(properties, nameof(properties)); - - index.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.IndexInclude, - properties, - fromDataAnnotation); - - return properties; - } - - /// - /// Returns the for the included property names. - /// - /// The index. - /// The for the included property names. - public static ConfigurationSource? GetIncludePropertiesConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.IndexInclude)?.GetConfigurationSource(); - - #endregion Included properties - - #region Created concurrently - - /// - /// Returns a value indicating whether the index is created concurrently. - /// - /// The index. - /// true if the index is created concurrently. - public static bool? IsCreatedConcurrently(this IReadOnlyIndex index) - => (bool?)index[NpgsqlAnnotationNames.CreatedConcurrently]; - - /// - /// Sets a value indicating whether the index is created concurrently. - /// - /// The index. - /// The value to set. - public static void SetIsCreatedConcurrently(this IMutableIndex index, bool? createdConcurrently) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.CreatedConcurrently, createdConcurrently); - - /// - /// Sets a value indicating whether the index is created concurrently. - /// - /// The index. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static bool? SetIsCreatedConcurrently( - this IConventionIndex index, - bool? createdConcurrently, - bool fromDataAnnotation = false) - { - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.CreatedConcurrently, createdConcurrently, fromDataAnnotation); - - return createdConcurrently; - } - - /// - /// Returns the for whether the index is created concurrently. - /// - /// The index. - /// The for whether the index is created concurrently. - public static ConfigurationSource? GetIsCreatedConcurrentlyConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.CreatedConcurrently)?.GetConfigurationSource(); - - #endregion Created concurrently - - #region NULLS distinct - - /// - /// Returns whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - public static bool? GetAreNullsDistinct(this IReadOnlyIndex index) - => (bool?)index[NpgsqlAnnotationNames.NullsDistinct]; - - /// - /// Sets whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - public static void SetAreNullsDistinct(this IMutableIndex index, bool? nullsDistinct) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.NullsDistinct, nullsDistinct); - - /// - /// Sets whether for a unique index, null values should be considered distinct (not equal). - /// The default is that they are distinct, so that a unique index could contain multiple null values in a column. - /// - /// - /// http://www.postgresql.org/docs/current/static/sql-createindex.html - /// - public static bool? SetAreNullsDistinct(this IConventionIndex index, bool? nullsDistinct, bool fromDataAnnotation = false) - { - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.NullsDistinct, nullsDistinct, fromDataAnnotation); - - return nullsDistinct; - } - - /// - /// Returns the for whether nulls are considered distinct. - /// - /// The index. - /// The . - public static ConfigurationSource? GetAreNullsDistinctConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.NullsDistinct)?.GetConfigurationSource(); - - #endregion NULLS distinct - - #region ToTsVector - - /// - /// Returns the text search configuration for this tsvector expression index, or null if this is not a - /// tsvector expression index. - /// - /// The index. - /// - /// https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX - /// - public static string? GetTsVectorConfig(this IReadOnlyIndex index) - => (string?)index[NpgsqlAnnotationNames.TsVectorConfig]; - - /// - /// Sets the text search configuration for this tsvector expression index, or null if this is not a - /// tsvector expression index. - /// - /// The index. - /// - /// - /// 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. - /// - /// - /// - /// https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX - /// - public static void SetTsVectorConfig(this IMutableIndex index, string? config) - => index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.TsVectorConfig, config); - - /// - /// Sets the index to tsvector config name to be used. - /// - /// The index. - /// - /// - /// 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. - /// - /// https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX - /// - public static string? SetTsVectorConfig( - this IConventionIndex index, - string? config, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(config, nameof(config)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.TsVectorConfig, config, fromDataAnnotation); - - return config; - } - - /// - /// Returns the for the tsvector config. - /// - /// The index. - /// The for the tsvector config. - public static ConfigurationSource? GetTsVectorConfigConfigurationSource(this IConventionIndex index) - => index.FindAnnotation(NpgsqlAnnotationNames.TsVectorConfig)?.GetConfigurationSource(); - - #endregion ToTsVector - - #region Storage parameters - - /// - /// Gets all storage parameters for the index. - /// - public static Dictionary GetStorageParameters(this IReadOnlyIndex index) - => index.GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) - .ToDictionary( - a => a.Name.Substring(NpgsqlAnnotationNames.StorageParameterPrefix.Length), - a => a.Value); - - /// - /// Gets a storage parameter for the index. - /// - public static string? GetStorageParameter(this IIndex index, string parameterName) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - return (string?)index[NpgsqlAnnotationNames.StorageParameterPrefix + parameterName]; - } - - /// - /// Sets a storage parameter on the index. - /// - public static void SetStorageParameter(this IMutableIndex index, string parameterName, object? parameterValue) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.StorageParameterPrefix + parameterName, parameterValue); - } - - /// - /// Sets a storage parameter on the index. - /// - public static object SetStorageParameter( - this IConventionIndex index, - string parameterName, - object? parameterValue, - bool fromDataAnnotation = false) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - index.SetOrRemoveAnnotation(NpgsqlAnnotationNames.StorageParameterPrefix + parameterName, parameterValue, fromDataAnnotation); - - return parameterName; - } - - /// - /// Gets the configuration source for a storage parameter for the table mapped to the entity type. - /// - public static ConfigurationSource? GetStorageParameterConfigurationSource(this IConventionIndex index, string parameterName) - { - Check.NotEmpty(parameterName, nameof(parameterName)); - - return index.FindAnnotation(NpgsqlAnnotationNames.StorageParameterPrefix + parameterName)?.GetConfigurationSource(); - } - - #endregion Storage parameters -} diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs deleted file mode 100644 index a69ccf8ac7..0000000000 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs +++ /dev/null @@ -1,519 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Model extension methods for Npgsql-specific metadata. -/// -/// -/// See Modeling entity types and relationships. -/// -public static class NpgsqlModelExtensions -{ - /// - /// The default name for the hi-lo sequence. - /// - public const string DefaultHiLoSequenceName = "EntityFrameworkHiLoSequence"; - - /// - /// The default prefix for sequences applied to properties. - /// - public const string DefaultSequenceNameSuffix = "Sequence"; - - #region HiLo - - /// - /// Returns the name to use for the default hi-lo sequence. - /// - /// The model. - /// The name to use for the default hi-lo sequence. - public static string GetHiLoSequenceName(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.HiLoSequenceName] - ?? DefaultHiLoSequenceName; - - /// - /// Sets the name to use for the default hi-lo sequence. - /// - /// The model. - /// The value to set. - public static void SetHiLoSequenceName(this IMutableModel model, string? name) - { - Check.NullButNotEmpty(name, nameof(name)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.HiLoSequenceName, name); - } - - /// - /// Sets the name to use for the default hi-lo sequence. - /// - /// The model. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static string? SetHiLoSequenceName( - this IConventionModel model, - string? name, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(name, nameof(name)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.HiLoSequenceName, name, fromDataAnnotation); - - return name; - } - - /// - /// Returns the for the default hi-lo sequence name. - /// - /// The model. - /// The for the default hi-lo sequence name. - public static ConfigurationSource? GetHiLoSequenceNameConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.HiLoSequenceName)?.GetConfigurationSource(); - - /// - /// Returns the schema to use for the default hi-lo sequence. - /// - /// - /// The model. - /// The schema to use for the default hi-lo sequence. - public static string? GetHiLoSequenceSchema(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.HiLoSequenceSchema]; - - /// - /// Sets the schema to use for the default hi-lo sequence. - /// - /// The model. - /// The value to set. - public static void SetHiLoSequenceSchema(this IMutableModel model, string? value) - { - Check.NullButNotEmpty(value, nameof(value)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema, value); - } - - /// - /// Sets the schema to use for the default hi-lo sequence. - /// - /// The model. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static string? SetHiLoSequenceSchema( - this IConventionModel model, - string? value, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(value, nameof(value)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema, value, fromDataAnnotation); - - return value; - } - - /// - /// Returns the for the default hi-lo sequence schema. - /// - /// The model. - /// The for the default hi-lo sequence schema. - public static ConfigurationSource? GetHiLoSequenceSchemaConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema)?.GetConfigurationSource(); - - #endregion - - #region Sequence - - /// - /// Returns the suffix to append to the name of automatically created sequences. - /// - /// The model. - /// The name to use for the default key value generation sequence. - public static string GetSequenceNameSuffix(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.SequenceNameSuffix] - ?? DefaultSequenceNameSuffix; - - /// - /// Sets the suffix to append to the name of automatically created sequences. - /// - /// The model. - /// The value to set. - public static void SetSequenceNameSuffix(this IMutableModel model, string? name) - { - Check.NullButNotEmpty(name, nameof(name)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix, name); - } - - /// - /// Sets the suffix to append to the name of automatically created sequences. - /// - /// The model. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static string? SetSequenceNameSuffix( - this IConventionModel model, - string? name, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(name, nameof(name)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix, name, fromDataAnnotation); - - return name; - } - - /// - /// Returns the for the default value generation sequence name suffix. - /// - /// The model. - /// The for the default key value generation sequence name. - public static ConfigurationSource? GetSequenceNameSuffixConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.SequenceNameSuffix)?.GetConfigurationSource(); - - /// - /// Returns the schema to use for the default value generation sequence. - /// - /// - /// The model. - /// The schema to use for the default key value generation sequence. - public static string? GetSequenceSchema(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.SequenceSchema]; - - /// - /// Sets the schema to use for the default key value generation sequence. - /// - /// The model. - /// The value to set. - public static void SetSequenceSchema(this IMutableModel model, string? value) - { - Check.NullButNotEmpty(value, nameof(value)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceSchema, value); - } - - /// - /// Sets the schema to use for the default key value generation sequence. - /// - /// The model. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static string? SetSequenceSchema( - this IConventionModel model, - string? value, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(value, nameof(value)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.SequenceSchema, value, fromDataAnnotation); - - return value; - } - - /// - /// Returns the for the default key value generation sequence schema. - /// - /// The model. - /// The for the default key value generation sequence schema. - public static ConfigurationSource? GetSequenceSchemaConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.SequenceSchema)?.GetConfigurationSource(); - - #endregion Sequence - - #region Value Generation Strategy - - /// - /// Returns the to use for properties - /// of keys in the model, unless the property has a strategy explicitly set. - /// - /// The model. - /// The default . - public static NpgsqlValueGenerationStrategy? GetValueGenerationStrategy(this IReadOnlyModel model) - => (NpgsqlValueGenerationStrategy?)model[NpgsqlAnnotationNames.ValueGenerationStrategy]; - - /// - /// Attempts to set the to use for properties - /// of keys in the model that don't have a strategy explicitly set. - /// - /// The model. - /// The value to set. - public static void SetValueGenerationStrategy(this IMutableModel model, NpgsqlValueGenerationStrategy? value) - => model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); - - /// - /// Attempts to set the to use for properties - /// of keys in the model that don't have a strategy explicitly set. - /// - /// The model. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static NpgsqlValueGenerationStrategy? SetValueGenerationStrategy( - this IConventionModel model, - NpgsqlValueGenerationStrategy? value, - bool fromDataAnnotation = false) - { - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation); - - return value; - } - - /// - /// Returns the for the default . - /// - /// The model. - /// The for the default . - public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); - - #endregion - - #region PostgreSQL Extensions - - /// - /// 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 PostgresExtension GetOrAddPostgresExtension( - this IMutableModel model, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, 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. - /// - public static IReadOnlyList GetPostgresExtensions(this IReadOnlyModel model) - => PostgresExtension.GetPostgresExtensions(model).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 static PostgresExtension GetOrAddPostgresExtension( - this IConventionModel model, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, version); - - #endregion - - #region Enum types - - /// - /// 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 IMutableModel 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 - /// 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 - /// 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 IReadOnlyList GetPostgresEnums(this IReadOnlyModel model) - => PostgresEnum.GetPostgresEnums(model).ToArray(); - - #endregion Enum types - - #region Range types - - /// - /// 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 PostgresRange GetOrAddPostgresRange( - this IMutableModel model, - string? schema, - string name, - string subtype, - string? canonicalFunction = null, - string? subtypeOpClass = null, - string? collation = null, - string? subtypeDiff = null) - => PostgresRange.GetOrAddPostgresRange( - model, - schema, - name, - subtype, - canonicalFunction, - subtypeOpClass, - collation, - subtypeDiff); - - /// - /// 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 IReadOnlyList PostgresRanges(this IReadOnlyModel model) - => PostgresRange.GetPostgresRanges(model).ToArray(); - - #endregion Range types - - #region Database Template - - /// - /// 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? GetDatabaseTemplate(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.DatabaseTemplate]; - - /// - /// 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 void SetDatabaseTemplate(this IMutableModel model, string? template) - => model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.DatabaseTemplate, template); - - /// - /// 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? SetDatabaseTemplate( - this IConventionModel model, - string? template, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(template, nameof(template)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.DatabaseTemplate, template, fromDataAnnotation); - - return template; - } - - /// - /// 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 ConfigurationSource? GetDatabaseTemplateConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.DatabaseTemplate)?.GetConfigurationSource(); - - #endregion - - #region Tablespace - - /// - /// 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? GetTablespace(this IReadOnlyModel model) - => (string?)model[NpgsqlAnnotationNames.Tablespace]; - - /// - /// 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 void SetTablespace(this IMutableModel model, string? tablespace) - => model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.Tablespace, tablespace); - - /// - /// 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? SetTablespace( - this IConventionModel model, - string? tablespace, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(tablespace, nameof(tablespace)); - - model.SetOrRemoveAnnotation(NpgsqlAnnotationNames.Tablespace, tablespace, fromDataAnnotation); - - return tablespace; - } - - /// - /// 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 ConfigurationSource? GetTablespaceConfigurationSource(this IConventionModel model) - => model.FindAnnotation(NpgsqlAnnotationNames.Tablespace)?.GetConfigurationSource(); - - #endregion - - #region Collation management - - /// - /// 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 PostgresCollation GetOrAddCollation( - this IMutableModel model, - string? schema, - string name, - string lcCollate, - string lcCtype, - string? provider = null, - bool? deterministic = null) - => PostgresCollation.GetOrAddCollation( - model, - schema, - name, - lcCollate, - lcCtype, - provider, - deterministic); - - /// - /// 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 IReadOnlyList GetCollations(this IReadOnlyModel model) - => PostgresCollation.GetCollations(model).ToArray(); - - #endregion Collation management -} diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs deleted file mode 100644 index 8e4869b7bb..0000000000 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlPropertyExtensions.cs +++ /dev/null @@ -1,1199 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Property extension methods for Npgsql-specific metadata. -/// -/// -/// See Modeling entity types and relationships. -/// -public static class NpgsqlPropertyExtensions -{ - #region Hi-lo - - /// - /// Returns the name to use for the hi-lo sequence. - /// - /// The property. - /// The name to use for the hi-lo sequence. - public static string? GetHiLoSequenceName(this IReadOnlyProperty property) - => (string?)property[NpgsqlAnnotationNames.HiLoSequenceName]; - - /// - /// Returns the name to use for the hi-lo sequence. - /// - /// The property. - /// The identifier of the store object. - /// The name to use for the hi-lo sequence. - public static string? GetHiLoSequenceName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - { - var annotation = property.FindAnnotation(NpgsqlAnnotationNames.HiLoSequenceName); - if (annotation is not null) - { - return (string?)annotation.Value; - } - - var sharedTableRootProperty = property.FindSharedStoreObjectRootProperty(storeObject); - return sharedTableRootProperty is not null - ? sharedTableRootProperty.GetHiLoSequenceName(storeObject) - : null; - } - - /// - /// Sets the name to use for the hi-lo sequence. - /// - /// The property. - /// The sequence name to use. - public static void SetHiLoSequenceName(this IMutableProperty property, string? name) - => property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.HiLoSequenceName, - Check.NullButNotEmpty(name, nameof(name))); - - /// - /// Sets the name to use for the hi-lo sequence. - /// - /// The property. - /// The sequence name to use. - /// Indicates whether the configuration was specified using a data annotation. - public static string? SetHiLoSequenceName( - this IConventionProperty property, - string? name, - bool fromDataAnnotation = false) - { - property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.HiLoSequenceName, - Check.NullButNotEmpty(name, nameof(name)), - fromDataAnnotation); - - return name; - } - - /// - /// Returns the for the hi-lo sequence name. - /// - /// The property. - /// The for the hi-lo sequence name. - public static ConfigurationSource? GetHiLoSequenceNameConfigurationSource(this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.HiLoSequenceName)?.GetConfigurationSource(); - - /// - /// Returns the schema to use for the hi-lo sequence. - /// - /// The property. - /// The schema to use for the hi-lo sequence. - public static string? GetHiLoSequenceSchema(this IReadOnlyProperty property) - => (string?)property[NpgsqlAnnotationNames.HiLoSequenceSchema]; - - /// - /// Returns the schema to use for the hi-lo sequence. - /// - /// The property. - /// The identifier of the store object. - /// The schema to use for the hi-lo sequence. - public static string? GetHiLoSequenceSchema(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - { - var annotation = property.FindAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema); - if (annotation is not null) - { - return (string?)annotation.Value; - } - - var sharedTableRootProperty = property.FindSharedStoreObjectRootProperty(storeObject); - return sharedTableRootProperty is not null - ? sharedTableRootProperty.GetHiLoSequenceSchema(storeObject) - : null; - } - - /// - /// Sets the schema to use for the hi-lo sequence. - /// - /// The property. - /// The schema to use. - public static void SetHiLoSequenceSchema(this IMutableProperty property, string? schema) - => property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.HiLoSequenceSchema, - Check.NullButNotEmpty(schema, nameof(schema))); - - /// - /// Sets the schema to use for the hi-lo sequence. - /// - /// The property. - /// The schema to use. - /// Indicates whether the configuration was specified using a data annotation. - public static string? SetHiLoSequenceSchema( - this IConventionProperty property, - string? schema, - bool fromDataAnnotation = false) - { - property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.HiLoSequenceSchema, - Check.NullButNotEmpty(schema, nameof(schema)), - fromDataAnnotation); - - return schema; - } - - /// - /// Returns the for the hi-lo sequence schema. - /// - /// The property. - /// The for the hi-lo sequence schema. - public static ConfigurationSource? GetHiLoSequenceSchemaConfigurationSource(this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.HiLoSequenceSchema)?.GetConfigurationSource(); - - /// - /// Finds the in the model to use for the hi-lo pattern. - /// - /// The property. - /// The sequence to use, or if no sequence exists in the model. - public static IReadOnlySequence? FindHiLoSequence(this IReadOnlyProperty property) - { - var model = property.DeclaringType.Model; - - var sequenceName = property.GetHiLoSequenceName() - ?? model.GetHiLoSequenceName(); - - var sequenceSchema = property.GetHiLoSequenceSchema() - ?? model.GetHiLoSequenceSchema(); - - return model.FindSequence(sequenceName, sequenceSchema); - } - - /// - /// Finds the in the model to use for the hi-lo pattern. - /// - /// The property. - /// The identifier of the store object. - /// The sequence to use, or if no sequence exists in the model. - public static IReadOnlySequence? FindHiLoSequence(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - { - var model = property.DeclaringType.Model; - - var sequenceName = property.GetHiLoSequenceName(storeObject) - ?? model.GetHiLoSequenceName(); - - var sequenceSchema = property.GetHiLoSequenceSchema(storeObject) - ?? model.GetHiLoSequenceSchema(); - - return model.FindSequence(sequenceName, sequenceSchema); - } - - /// - /// Finds the in the model to use for the hi-lo pattern. - /// - /// The property. - /// The sequence to use, or if no sequence exists in the model. - public static ISequence? FindHiLoSequence(this IProperty property) - => (ISequence?)((IReadOnlyProperty)property).FindHiLoSequence(); - - /// - /// Finds the in the model to use for the hi-lo pattern. - /// - /// The property. - /// The identifier of the store object. - /// The sequence to use, or if no sequence exists in the model. - public static ISequence? FindHiLoSequence(this IProperty property, in StoreObjectIdentifier storeObject) - => (ISequence?)((IReadOnlyProperty)property).FindHiLoSequence(storeObject); - - /// - /// Removes all identity sequence annotations from the property. - /// - public static void RemoveHiLoOptions(this IMutableProperty property) - { - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); - } - - /// - /// Removes all identity sequence annotations from the property. - /// - public static void RemoveHiLoOptions(this IConventionProperty property) - { - property.SetHiLoSequenceName(null); - property.SetHiLoSequenceSchema(null); - } - - #endregion Hi-lo - - #region Sequence - - /// - /// Returns the name to use for the key value generation sequence. - /// - /// The property. - /// The name to use for the key value generation sequence. - public static string? GetSequenceName(this IReadOnlyProperty property) - => (string?)property[NpgsqlAnnotationNames.SequenceName]; - - /// - /// Returns the name to use for the key value generation sequence. - /// - /// The property. - /// The identifier of the store object. - /// The name to use for the key value generation sequence. - public static string? GetSequenceName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - { - var annotation = property.FindAnnotation(NpgsqlAnnotationNames.SequenceName); - if (annotation != null) - { - return (string?)annotation.Value; - } - - return property.FindSharedStoreObjectRootProperty(storeObject)?.GetSequenceName(storeObject); - } - - /// - /// Sets the name to use for the key value generation sequence. - /// - /// The property. - /// The sequence name to use. - public static void SetSequenceName(this IMutableProperty property, string? name) - => property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.SequenceName, - Check.NullButNotEmpty(name, nameof(name))); - - /// - /// Sets the name to use for the key value generation sequence. - /// - /// The property. - /// The sequence name to use. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static string? SetSequenceName( - this IConventionProperty property, - string? name, - bool fromDataAnnotation = false) - { - property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.SequenceName, - Check.NullButNotEmpty(name, nameof(name)), - fromDataAnnotation); - - return name; - } - - /// - /// Returns the for the key value generation sequence name. - /// - /// The property. - /// The for the key value generation sequence name. - public static ConfigurationSource? GetSequenceNameConfigurationSource(this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.SequenceName)?.GetConfigurationSource(); - - /// - /// Returns the schema to use for the key value generation sequence. - /// - /// The property. - /// The schema to use for the key value generation sequence. - public static string? GetSequenceSchema(this IReadOnlyProperty property) - => (string?)property[NpgsqlAnnotationNames.SequenceSchema]; - - /// - /// Returns the schema to use for the key value generation sequence. - /// - /// The property. - /// The identifier of the store object. - /// The schema to use for the key value generation sequence. - public static string? GetSequenceSchema(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - { - var annotation = property.FindAnnotation(NpgsqlAnnotationNames.SequenceSchema); - if (annotation != null) - { - return (string?)annotation.Value; - } - - return property.FindSharedStoreObjectRootProperty(storeObject)?.GetSequenceSchema(storeObject); - } - - /// - /// Sets the schema to use for the key value generation sequence. - /// - /// The property. - /// The schema to use. - public static void SetSequenceSchema(this IMutableProperty property, string? schema) - => property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.SequenceSchema, - Check.NullButNotEmpty(schema, nameof(schema))); - - /// - /// Sets the schema to use for the key value generation sequence. - /// - /// The property. - /// The schema to use. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static string? SetSequenceSchema( - this IConventionProperty property, - string? schema, - bool fromDataAnnotation = false) - { - property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.SequenceSchema, - Check.NullButNotEmpty(schema, nameof(schema)), - fromDataAnnotation); - - return schema; - } - - /// - /// Returns the for the key value generation sequence schema. - /// - /// The property. - /// The for the key value generation sequence schema. - public static ConfigurationSource? GetSequenceSchemaConfigurationSource(this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.SequenceSchema)?.GetConfigurationSource(); - - /// - /// Finds the in the model to use for the key value generation pattern. - /// - /// The property. - /// The sequence to use, or if no sequence exists in the model. - public static IReadOnlySequence? FindSequence(this IReadOnlyProperty property) - { - var model = property.DeclaringType.Model; - - var sequenceName = property.GetSequenceName() - ?? model.GetSequenceNameSuffix(); - - var sequenceSchema = property.GetSequenceSchema() - ?? model.GetSequenceSchema(); - - return model.FindSequence(sequenceName, sequenceSchema); - } - - /// - /// Finds the in the model to use for the key value generation pattern. - /// - /// The property. - /// The identifier of the store object. - /// The sequence to use, or if no sequence exists in the model. - public static IReadOnlySequence? FindSequence(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - { - var model = property.DeclaringType.Model; - - var sequenceName = property.GetSequenceName(storeObject) - ?? model.GetSequenceNameSuffix(); - - var sequenceSchema = property.GetSequenceSchema(storeObject) - ?? model.GetSequenceSchema(); - - return model.FindSequence(sequenceName, sequenceSchema); - } - - /// - /// Finds the in the model to use for the key value generation pattern. - /// - /// The property. - /// The sequence to use, or if no sequence exists in the model. - public static ISequence? FindSequence(this IProperty property) - => (ISequence?)((IReadOnlyProperty)property).FindSequence(); - - /// - /// Finds the in the model to use for the key value generation pattern. - /// - /// The property. - /// The identifier of the store object. - /// The sequence to use, or if no sequence exists in the model. - public static ISequence? FindSequence(this IProperty property, in StoreObjectIdentifier storeObject) - => (ISequence?)((IReadOnlyProperty)property).FindSequence(storeObject); - - #endregion Sequence - - #region Value generation - - /// - /// Returns the to use for the property. - /// - /// If no strategy is set for the property, then the strategy to use will be taken from the . - /// - /// - /// The strategy, or if none was set. - public static NpgsqlValueGenerationStrategy GetValueGenerationStrategy(this IReadOnlyProperty property) - { - var annotation = property.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy); - if (annotation != null) - { - return (NpgsqlValueGenerationStrategy?)annotation.Value ?? NpgsqlValueGenerationStrategy.None; - } - - var defaultValueGenerationStrategy = GetDefaultValueGenerationStrategy(property); - if (property.ValueGenerated != ValueGenerated.OnAdd - || property.IsForeignKey() - || property.TryGetDefaultValue(out _) - || (defaultValueGenerationStrategy != NpgsqlValueGenerationStrategy.Sequence && property.GetDefaultValueSql() != null) - || property.GetComputedColumnSql() is not null) - { - return NpgsqlValueGenerationStrategy.None; - } - - return defaultValueGenerationStrategy; - } - - /// - /// Returns the to use for the property. - /// - /// If no strategy is set for the property, then the strategy to use will be taken from the . - /// - /// - /// The property. - /// The identifier of the store object. - /// The strategy, or if none was set. - public static NpgsqlValueGenerationStrategy GetValueGenerationStrategy( - this IReadOnlyProperty property, - in StoreObjectIdentifier storeObject) - => GetValueGenerationStrategy(property, storeObject, null); - - internal static NpgsqlValueGenerationStrategy GetValueGenerationStrategy( - this IReadOnlyProperty property, - in StoreObjectIdentifier storeObject, - ITypeMappingSource? typeMappingSource) - { - var @override = property.FindOverrides(storeObject)?.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy); - if (@override != null) - { - return (NpgsqlValueGenerationStrategy?)@override.Value ?? NpgsqlValueGenerationStrategy.None; - } - - var annotation = property.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy); - if (annotation?.Value != null - && StoreObjectIdentifier.Create(property.DeclaringType, storeObject.StoreObjectType) == storeObject) - { - return (NpgsqlValueGenerationStrategy)annotation.Value; - } - - var table = storeObject; - var sharedTableRootProperty = property.FindSharedStoreObjectRootProperty(storeObject); - if (sharedTableRootProperty != null) - { - return sharedTableRootProperty.GetValueGenerationStrategy(storeObject, typeMappingSource) is var npgsqlValueGenerationStrategy - && npgsqlValueGenerationStrategy is - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn - or NpgsqlValueGenerationStrategy.SerialColumn - && table.StoreObjectType == StoreObjectType.Table - && !property.GetContainingForeignKeys().Any( - fk => - !fk.IsBaseLinking() - || (StoreObjectIdentifier.Create(fk.PrincipalEntityType, StoreObjectType.Table) - is StoreObjectIdentifier principal - && fk.GetConstraintName(table, principal) != null)) - ? npgsqlValueGenerationStrategy - : NpgsqlValueGenerationStrategy.None; - } - - if (property.ValueGenerated != ValueGenerated.OnAdd - || table.StoreObjectType != StoreObjectType.Table - || property.TryGetDefaultValue(storeObject, out _) - || property.GetDefaultValueSql(storeObject) != null - || property.GetComputedColumnSql(storeObject) != null - || property.GetContainingForeignKeys() - .Any( - fk => - !fk.IsBaseLinking() - || (StoreObjectIdentifier.Create(fk.PrincipalEntityType, StoreObjectType.Table) - is StoreObjectIdentifier principal - && fk.GetConstraintName(table, principal) != null))) - { - return NpgsqlValueGenerationStrategy.None; - } - - var defaultStrategy = GetDefaultValueGenerationStrategy(property, storeObject, typeMappingSource); - if (defaultStrategy != NpgsqlValueGenerationStrategy.None) - { - if (annotation != null) - { - return (NpgsqlValueGenerationStrategy?)annotation.Value ?? NpgsqlValueGenerationStrategy.None; - } - } - - return defaultStrategy; - } - - /// - /// Returns the to use for the property. - /// - /// - /// If no strategy is set for the property, then the strategy to use will be taken from the . - /// - /// The property overrides. - /// The strategy, or if none was set. - public static NpgsqlValueGenerationStrategy? GetValueGenerationStrategy( - this IReadOnlyRelationalPropertyOverrides overrides) - => (NpgsqlValueGenerationStrategy?)overrides.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy) - ?.Value; - - private static NpgsqlValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property) - { - var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); - - switch (modelStrategy) - { - case NpgsqlValueGenerationStrategy.SequenceHiLo: - case NpgsqlValueGenerationStrategy.SerialColumn: - case NpgsqlValueGenerationStrategy.Sequence: - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - return IsCompatibleWithValueGeneration(property) - ? modelStrategy.Value - : NpgsqlValueGenerationStrategy.None; - case NpgsqlValueGenerationStrategy.None: - case null: - return NpgsqlValueGenerationStrategy.None; - default: - throw new ArgumentOutOfRangeException(); - } - } - - private static NpgsqlValueGenerationStrategy GetDefaultValueGenerationStrategy( - IReadOnlyProperty property, - in StoreObjectIdentifier storeObject, - ITypeMappingSource? typeMappingSource) - { - var modelStrategy = property.DeclaringType.Model.GetValueGenerationStrategy(); - - switch (modelStrategy) - { - case NpgsqlValueGenerationStrategy.SequenceHiLo: - return IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource) - ? modelStrategy.Value - : NpgsqlValueGenerationStrategy.None; - - case NpgsqlValueGenerationStrategy.SerialColumn: - case NpgsqlValueGenerationStrategy.Sequence: - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - return !IsCompatibleWithValueGeneration(property, storeObject, typeMappingSource) - ? NpgsqlValueGenerationStrategy.None - : property.DeclaringType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy - ? NpgsqlValueGenerationStrategy.Sequence - : modelStrategy.Value; - - case NpgsqlValueGenerationStrategy.None: - case null: - return NpgsqlValueGenerationStrategy.None; - - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Sets the to use for the property. - /// - /// The property. - /// The strategy to use. - public static void SetValueGenerationStrategy( - this IMutableProperty property, - NpgsqlValueGenerationStrategy? value) - => property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); - - /// - /// Sets the to use for the property. - /// - /// The property. - /// The strategy to use. - /// Indicates whether the configuration was specified using a data annotation. - public static NpgsqlValueGenerationStrategy? SetValueGenerationStrategy( - this IConventionProperty property, - NpgsqlValueGenerationStrategy? value, - bool fromDataAnnotation = false) - => (NpgsqlValueGenerationStrategy?)property.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value; - - /// - /// Sets the to use for the property for a particular table. - /// - /// The property. - /// The strategy to use. - /// The identifier of the table containing the column. - public static void SetValueGenerationStrategy( - this IMutableProperty property, - NpgsqlValueGenerationStrategy? value, - in StoreObjectIdentifier storeObject) - => property.GetOrCreateOverrides(storeObject) - .SetValueGenerationStrategy(value); - - /// - /// Sets the to use for the property for a particular table. - /// - /// The property. - /// The strategy to use. - /// The identifier of the table containing the column. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static NpgsqlValueGenerationStrategy? SetValueGenerationStrategy( - this IConventionProperty property, - NpgsqlValueGenerationStrategy? value, - in StoreObjectIdentifier storeObject, - bool fromDataAnnotation = false) - => property.GetOrCreateOverrides(storeObject, fromDataAnnotation) - .SetValueGenerationStrategy(value, fromDataAnnotation); - - /// - /// Sets the to use for the property for a particular table. - /// - /// The property overrides. - /// The strategy to use. - public static void SetValueGenerationStrategy( - this IMutableRelationalPropertyOverrides overrides, - NpgsqlValueGenerationStrategy? value) - => overrides.SetOrRemoveAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy, value); - - /// - /// Sets the to use for the property for a particular table. - /// - /// The property overrides. - /// The strategy to use. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static NpgsqlValueGenerationStrategy? SetValueGenerationStrategy( - this IConventionRelationalPropertyOverrides overrides, - NpgsqlValueGenerationStrategy? value, - bool fromDataAnnotation = false) - => (NpgsqlValueGenerationStrategy?)overrides.SetOrRemoveAnnotation( - NpgsqlAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation)?.Value; - - /// - /// Returns the for the . - /// - /// The property. - /// The for the . - public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); - - /// - /// Returns the for the for a particular table. - /// - /// The property. - /// The identifier of the table containing the column. - /// The for the . - public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( - this IConventionProperty property, - in StoreObjectIdentifier storeObject) - => property.FindOverrides(storeObject)?.GetValueGenerationStrategyConfigurationSource(); - - /// - /// Returns the for the for a particular table. - /// - /// The property overrides. - /// The for the . - public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( - this IConventionRelationalPropertyOverrides overrides) - => overrides.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); - - /// - /// Returns a value indicating whether the property is compatible with any . - /// - /// The property. - /// if compatible. - public static bool IsCompatibleWithValueGeneration(IReadOnlyProperty property) - { - var valueConverter = property.GetValueConverter() - ?? property.FindTypeMapping()?.Converter; - - var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); - - return type.IsInteger() || type.IsEnum; - } - - private static bool IsCompatibleWithValueGeneration( - IReadOnlyProperty property, - in StoreObjectIdentifier storeObject, - ITypeMappingSource? typeMappingSource) - { - var valueConverter = property.GetValueConverter() - ?? (property.FindRelationalTypeMapping(storeObject) - ?? typeMappingSource?.FindMapping((IProperty)property))?.Converter; - - var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); - - return type.IsInteger() || type.IsEnum; - } - - #endregion Value generation - - #region Identity sequence options - - /// - /// Returns the identity start value. - /// - /// The property. - /// The identity start value. - public static long? GetIdentityStartValue(this IReadOnlyProperty property) - => IdentitySequenceOptionsData.Get(property).StartValue; - - /// - /// Sets the identity start value. - /// - /// The property. - /// The value to set. - public static void SetIdentityStartValue(this IMutableProperty property, long? startValue) - { - var options = IdentitySequenceOptionsData.Get(property); - options.StartValue = startValue; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize()); - } - - /// - /// Sets the identity start value. - /// - /// The property. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static long? SetIdentityStartValue( - this IConventionProperty property, - long? startValue, - bool fromDataAnnotation = false) - { - var options = IdentitySequenceOptionsData.Get(property); - options.StartValue = startValue; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); - return startValue; - } - - /// - /// Returns the for the identity start value. - /// - /// The property. - /// The for the identity start value. - public static ConfigurationSource? GetIdentityStartValueConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.IdentityOptions)?.GetConfigurationSource(); - - /// - /// Returns the identity increment value. - /// - /// The property. - /// The identity increment value. - public static long? GetIdentityIncrementBy(this IReadOnlyProperty property) - => IdentitySequenceOptionsData.Get(property).IncrementBy; - - /// - /// Sets the identity increment value. - /// - /// The property. - /// The value to set. - public static void SetIdentityIncrementBy(this IMutableProperty property, long? incrementBy) - { - var options = IdentitySequenceOptionsData.Get(property); - options.IncrementBy = incrementBy ?? 1; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize()); - } - - /// - /// Sets the identity increment value. - /// - /// The property. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static long? SetIdentityIncrementBy( - this IConventionProperty property, - long? incrementBy, - bool fromDataAnnotation = false) - { - var options = IdentitySequenceOptionsData.Get(property); - options.IncrementBy = incrementBy ?? 1; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); - return incrementBy; - } - - /// - /// Returns the for the identity increment value. - /// - /// The property. - /// The for the identity increment value. - public static ConfigurationSource? GetIdentityIncrementByConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.IdentityOptions)?.GetConfigurationSource(); - - /// - /// Returns the identity minimum value. - /// - /// The property. - /// The identity minimum value. - public static long? GetIdentityMinValue(this IReadOnlyProperty property) - => IdentitySequenceOptionsData.Get(property).MinValue; - - /// - /// Sets the identity minimum value. - /// - /// The property. - /// The value to set. - public static void SetIdentityMinValue(this IMutableProperty property, long? minValue) - { - var options = IdentitySequenceOptionsData.Get(property); - options.MinValue = minValue; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize()); - } - - /// - /// Sets the identity minimum value. - /// - /// The property. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static long? SetIdentityMinValue( - this IConventionProperty property, - long? minValue, - bool fromDataAnnotation = false) - { - var options = IdentitySequenceOptionsData.Get(property); - options.MinValue = minValue; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); - return minValue; - } - - /// - /// Returns the for the identity minimum value. - /// - /// The property. - /// The for the identity minimum value. - public static ConfigurationSource? GetIdentityMinValueConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.IdentityOptions)?.GetConfigurationSource(); - - /// - /// Returns the identity maximum value. - /// - /// The property. - /// The identity maximum value. - public static long? GetIdentityMaxValue(this IReadOnlyProperty property) - => IdentitySequenceOptionsData.Get(property).MaxValue; - - /// - /// Sets the identity maximum value. - /// - /// The property. - /// The value to set. - public static void SetIdentityMaxValue(this IMutableProperty property, long? maxValue) - { - var options = IdentitySequenceOptionsData.Get(property); - options.MaxValue = maxValue; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize()); - } - - /// - /// Sets the identity maximum value. - /// - /// The property. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static long? SetIdentityMaxValue( - this IConventionProperty property, - long? maxValue, - bool fromDataAnnotation = false) - { - var options = IdentitySequenceOptionsData.Get(property); - options.MaxValue = maxValue; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); - return maxValue; - } - - /// - /// Returns the for the identity maximum value. - /// - /// The property. - /// The for the identity maximum value. - public static ConfigurationSource? GetIdentityMaxValueConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.IdentityOptions)?.GetConfigurationSource(); - - /// - /// Returns whether the identity's sequence is cyclic. - /// - /// The property. - /// Whether the identity's sequence is cyclic. - public static bool? GetIdentityIsCyclic(this IReadOnlyProperty property) - => IdentitySequenceOptionsData.Get(property).IsCyclic; - - /// - /// Sets whether the identity's sequence is cyclic. - /// - /// The property. - /// The value to set. - public static void SetIdentityIsCyclic(this IMutableProperty property, bool? cyclic) - { - var options = IdentitySequenceOptionsData.Get(property); - options.IsCyclic = cyclic ?? false; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize()); - } - - /// - /// Sets whether the identity's sequence is cyclic. - /// - /// The property. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static bool? SetIdentityIsCyclic( - this IConventionProperty property, - bool? cyclic, - bool fromDataAnnotation = false) - { - var options = IdentitySequenceOptionsData.Get(property); - options.IsCyclic = cyclic ?? false; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); - return cyclic; - } - - /// - /// Returns the for whether the identity's sequence is cyclic. - /// - /// The property. - /// The for whether the identity's sequence is cyclic. - public static ConfigurationSource? GetIdentityIsCyclicConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.IdentityOptions)?.GetConfigurationSource(); - - /// - /// Returns the number of sequence numbers to be preallocated and stored in memory for faster access. - /// Defaults to 1 (no cache). - /// - /// The property. - /// The number of sequence numbers to be cached. - public static long? GetIdentityNumbersToCache(this IReadOnlyProperty property) - => IdentitySequenceOptionsData.Get(property).NumbersToCache; - - /// - /// Sets the number of sequence numbers to be preallocated and stored in memory for faster access. - /// - /// The property. - /// The value to set. - public static void SetIdentityNumbersToCache(this IMutableProperty property, long? numbersToCache) - { - var options = IdentitySequenceOptionsData.Get(property); - options.NumbersToCache = numbersToCache ?? 1; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize()); - } - - /// - /// Sets the number of sequence numbers to be preallocated and stored in memory for faster access. - /// - /// The property. - /// The value to set. - /// Indicates whether the configuration was specified using a data annotation. - public static long? SetIdentityNumbersToCache( - this IConventionProperty property, - long? numbersToCache, - bool fromDataAnnotation = false) - { - var options = IdentitySequenceOptionsData.Get(property); - options.NumbersToCache = numbersToCache ?? 1; - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions, options.Serialize(), fromDataAnnotation); - return numbersToCache; - } - - /// - /// Returns the for the number of sequence numbers to be preallocated - /// and stored in memory for faster access. - /// - /// The property. - /// - /// The for the number of sequence numbers to be preallocated - /// and stored in memory for faster access. - /// - public static ConfigurationSource? GetIdentityNumbersToCacheConfigurationSource( - this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.IdentityOptions)?.GetConfigurationSource(); - - /// - /// Removes identity sequence options from the property. - /// - public static void RemoveIdentityOptions(this IMutableProperty property) - => property.RemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions); - - /// - /// Removes identity sequence options from the property. - /// - public static void RemoveIdentityOptions(this IConventionProperty property) - => property.RemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions); - - #endregion Identity sequence options - - #region Generated tsvector column - - /// - /// Returns the text search configuration for this generated tsvector property, or null if this is not a - /// generated tsvector property. - /// - /// The property. - /// - /// - /// 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. - /// - /// - public static string? GetTsVectorConfig(this IReadOnlyProperty property) - => (string?)property[NpgsqlAnnotationNames.TsVectorConfig]; - - /// - /// Sets the text search configuration for this generated tsvector property, or null if this is not a - /// generated tsvector property. - /// - /// The property. - /// - /// - /// 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. - /// - /// - public static void SetTsVectorConfig(this IMutableProperty property, string? config) - { - Check.NullButNotEmpty(config, nameof(config)); - - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.TsVectorConfig, config); - } - - /// - /// Returns the text search configuration for this generated tsvector property, or null if this is not a - /// generated tsvector property. - /// - /// The property. - /// Indicates whether the configuration was specified using a data annotation. - /// - /// - /// 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. - /// - /// - public static string SetTsVectorConfig( - this IConventionProperty property, - string config, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(config, nameof(config)); - - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.TsVectorConfig, config, fromDataAnnotation); - - return config; - } - - /// - /// Returns the for the text search configuration for the generated tsvector - /// property. - /// - /// The property. - /// The configuration source for the text search configuration for the generated tsvector property. - public static ConfigurationSource? GetTsVectorConfigConfigurationSource(this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.TsVectorConfig)?.GetConfigurationSource(); - - /// - /// Returns the properties included in this generated tsvector property, or null if this is not a - /// generated tsvector property. - /// - /// The property. - /// The included property names, or null if this is not a Generated tsvector column. - public static IReadOnlyList? GetTsVectorProperties(this IReadOnlyProperty property) - => (string[]?)property[NpgsqlAnnotationNames.TsVectorProperties]; - - /// - /// Sets the properties included in this generated tsvector property, or null to make this a regular, - /// non-generated property. - /// - /// The property. - /// The included property names. - public static void SetTsVectorProperties( - this IMutableProperty property, - IReadOnlyList? properties) - { - Check.NullButNotEmpty(properties, nameof(properties)); - - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.TsVectorProperties, properties); - } - - /// - /// Sets properties included in this generated tsvector property, or null to make this a regular, - /// non-generated property. - /// - /// The property. - /// Indicates whether the configuration was specified using a data annotation. - /// The included property names. - public static IReadOnlyList? SetTsVectorProperties( - this IConventionProperty property, - IReadOnlyList? properties, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(properties, nameof(properties)); - - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.TsVectorProperties, properties, fromDataAnnotation); - - return properties; - } - - /// - /// Returns the for the properties included in the generated tsvector property. - /// - /// The property. - /// The configuration source for the properties included in the generated tsvector property. - public static ConfigurationSource? GetTsVectorPropertiesConfigurationSource(this IConventionProperty property) - => property.FindAnnotation(NpgsqlAnnotationNames.TsVectorConfig)?.GetConfigurationSource(); - - #endregion Generated tsvector column - - #region Compression method - - /// - /// Returns the compression method to be used, or null if it hasn't been specified. - /// - /// This feature was introduced in PostgreSQL 14. - public static string? GetCompressionMethod(this IReadOnlyProperty property) - => (property is RuntimeProperty) - ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) - : (string?)property[NpgsqlAnnotationNames.CompressionMethod]; - - /// - /// Returns the compression method to be used, or null if it hasn't been specified. - /// - /// This feature was introduced in PostgreSQL 14. - public static string? GetCompressionMethod(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - => property is RuntimeProperty - ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) - : property.FindAnnotation(NpgsqlAnnotationNames.CompressionMethod) is { } annotation - ? (string?)annotation.Value - : property.FindSharedStoreObjectRootProperty(storeObject)?.GetCompressionMethod(storeObject); - - /// - /// Sets the compression method to be used, or null if it hasn't been specified. - /// - /// This feature was introduced in PostgreSQL 14. - public static void SetCompressionMethod(this IMutableProperty property, string? compressionMethod) - => property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.CompressionMethod, compressionMethod); - - /// - /// Sets the compression method to be used, or null if it hasn't been specified. - /// - /// This feature was introduced in PostgreSQL 14. - public static string? SetCompressionMethod( - this IConventionProperty property, - string? compressionMethod, - bool fromDataAnnotation = false) - { - Check.NullButNotEmpty(compressionMethod, nameof(compressionMethod)); - - property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.CompressionMethod, compressionMethod, fromDataAnnotation); - - return compressionMethod; - } - - /// - /// Returns the for the compression method. - /// - /// The property. - /// The for the compression method. - public static ConfigurationSource? GetCompressionMethodConfigurationSource(this IConventionProperty index) - => index.FindAnnotation(NpgsqlAnnotationNames.IndexMethod)?.GetConfigurationSource(); - - #endregion Compression method -} diff --git a/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs b/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs deleted file mode 100644 index c061bf1550..0000000000 --- a/src/EFCore.PG/Extensions/NpgsqlAlterDatabaseOperationAnnotations.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Extension methods for for Npgsql-specific metadata. -/// -public static class NpgsqlAlterDatabaseOperationExtensions -{ - /// - /// 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 IReadOnlyList GetPostgresCollations(this AlterDatabaseOperation operation) - => PostgresCollation.GetCollations(operation).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 static IReadOnlyList GetOldPostgresCollations(this AlterDatabaseOperation operation) - => PostgresCollation.GetCollations(operation.OldDatabase).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 static IReadOnlyList GetPostgresExtensions(this AlterDatabaseOperation operation) - => PostgresExtension.GetPostgresExtensions(operation).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 static IReadOnlyList GetOldPostgresExtensions(this AlterDatabaseOperation operation) - => PostgresExtension.GetPostgresExtensions(operation.OldDatabase).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 static IReadOnlyList GetPostgresEnums(this AlterDatabaseOperation operation) - => PostgresEnum.GetPostgresEnums(operation).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 static IReadOnlyList GetOldPostgresEnums(this AlterDatabaseOperation operation) - => PostgresEnum.GetPostgresEnums(operation.OldDatabase).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 static IReadOnlyList GetPostgresRanges(this AlterDatabaseOperation operation) - => PostgresRange.GetPostgresRanges(operation).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 static IReadOnlyList GetOldPostgresRanges(this AlterDatabaseOperation operation) - => PostgresRange.GetPostgresRanges(operation.OldDatabase).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 static PostgresExtension GetOrAddPostgresExtension( - this AlterDatabaseOperation operation, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(operation, schema, name, version); -} diff --git a/src/EFCore.PG/Extensions/NpgsqlDatabaseFacadeExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlDatabaseFacadeExtensions.cs deleted file mode 100644 index eb2ddee5ae..0000000000 --- a/src/EFCore.PG/Extensions/NpgsqlDatabaseFacadeExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Data.Common; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Npgsql specific extension methods for . -/// -public static class NpgsqlDatabaseFacadeExtensions -{ - /// - /// - /// Returns true if the database provider currently in use is the Npgsql provider. - /// - /// - /// This method can only be used after the has been configured because - /// it is only then that the provider is known. This means that this method cannot be used - /// in because this is where application code sets the - /// provider to use as part of configuring the context. - /// - /// - /// The facade from . - /// True if Npgsql is being used; false otherwise. - public static bool IsNpgsql(this DatabaseFacade database) - => database.ProviderName == typeof(NpgsqlOptionsExtension).GetTypeInfo().Assembly.GetName().Name; - - /// - /// Sets the underlying configured for this . - /// - /// - /// - /// It may not be possible to change the data source if existing connection, if any, is open. - /// - /// - /// See Connections and connection strings for more information and examples. - /// - /// - /// The for the context. - /// The connection string. - public static void SetDbDataSource(this DatabaseFacade databaseFacade, DbDataSource dataSource) - => ((NpgsqlRelationalConnection)GetFacadeDependencies(databaseFacade).RelationalConnection).DbDataSource = dataSource; - - private static IRelationalDatabaseFacadeDependencies GetFacadeDependencies(DatabaseFacade databaseFacade) - { - var dependencies = ((IDatabaseFacadeDependenciesAccessor)databaseFacade).Dependencies; - - if (dependencies is IRelationalDatabaseFacadeDependencies relationalDependencies) - { - return relationalDependencies; - } - - throw new InvalidOperationException(RelationalStrings.RelationalNotInUse); - } -} diff --git a/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs deleted file mode 100644 index 3c82a0626f..0000000000 --- a/src/EFCore.PG/Extensions/NpgsqlDatabaseModelExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// 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 NpgsqlDatabaseModelExtensions -{ - /// - /// 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 PostgresExtension GetOrAddPostgresExtension( - this DatabaseModel model, - string? schema, - string name, - string? version) - => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, 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. - /// - public static IReadOnlyList GetPostgresExtensions(this DatabaseModel model) - => PostgresExtension.GetPostgresExtensions(model).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 static IReadOnlyList GetPostgresEnums(this DatabaseModel model) - => PostgresEnum.GetPostgresEnums(model).ToArray(); -} diff --git a/src/EFCore.PG/Extensions/NpgsqlDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlDbContextOptionsBuilderExtensions.cs deleted file mode 100644 index b77ad37d88..0000000000 --- a/src/EFCore.PG/Extensions/NpgsqlDbContextOptionsBuilderExtensions.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Data.Common; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Provides extension methods on and -/// used to configure a to context to a PostgreSQL database with Npgsql. -/// -public static class NpgsqlDbContextOptionsBuilderExtensions -{ - /// - /// - /// Configures the context to connect to a PostgreSQL server with Npgsql, but without initially setting any - /// or connection string. - /// - /// - /// The connection or connection string must be set before the is used to connect - /// to a database. Set a connection using . - /// Set a connection string using . - /// - /// - /// The builder being used to configure the context. - /// An optional action to allow additional Npgsql-specific configuration. - /// The options builder so that further configuration can be chained. - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - Action? npgsqlOptionsAction = null) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(GetOrCreateExtension(optionsBuilder)); - - ConfigureWarnings(optionsBuilder); - - npgsqlOptionsAction?.Invoke(new NpgsqlDbContextOptionsBuilder(optionsBuilder)); - - return optionsBuilder; - } - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// A builder for setting options on the context. - /// The connection string of the database to connect to. - /// An optional action to allow additional Npgsql-specific configuration. - /// - /// The options builder so that further configuration can be chained. - /// - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - string? connectionString, - Action? npgsqlOptionsAction = null) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - - var extension = (NpgsqlOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnectionString(connectionString); - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); - - ConfigureWarnings(optionsBuilder); - - npgsqlOptionsAction?.Invoke(new NpgsqlDbContextOptionsBuilder(optionsBuilder)); - - return optionsBuilder; - } - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// The builder being used to configure the context. - /// - /// An existing to be used to connect to the database. If the connection is - /// in the open state then EF will not open or close the connection. If the connection is in the closed - /// state then EF will open and close the connection as needed. The caller owns the connection and is - /// responsible for its disposal. - /// - /// An optional action to allow additional Npgsql-specific configuration. - /// The options builder so that further configuration can be chained. - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - DbConnection connection, - Action? npgsqlOptionsAction = null) - => UseNpgsql(optionsBuilder, connection, contextOwnsConnection: false, npgsqlOptionsAction); - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// A builder for setting options on the context. - /// - /// An existing to be used to connect to the database. If the connection is - /// in the open state then EF will not open or close the connection. If the connection is in the closed - /// state then EF will open and close the connection as needed. - /// - /// - /// If , then EF will take ownership of the connection and will - /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still - /// owns the connection and is responsible for its disposal. - /// - /// An optional action to allow additional Npgsql-specific configuration. - /// - /// The options builder so that further configuration can be chained. - /// - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - DbConnection connection, - bool contextOwnsConnection, - Action? npgsqlOptionsAction = null) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotNull(connection, nameof(connection)); - - var extension = (NpgsqlOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnection(connection, contextOwnsConnection); - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); - - ConfigureWarnings(optionsBuilder); - - npgsqlOptionsAction?.Invoke(new NpgsqlDbContextOptionsBuilder(optionsBuilder)); - - return optionsBuilder; - } - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// A builder for setting options on the context. - /// A which will be used to get database connections. - /// An optional action to allow additional Npgsql-specific configuration. - /// - /// The options builder so that further configuration can be chained. - /// - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - DbDataSource dataSource, - Action? npgsqlOptionsAction = null) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotNull(dataSource, nameof(dataSource)); - - var extension = (NpgsqlOptionsExtension)GetOrCreateExtension(optionsBuilder).WithDataSource(dataSource); - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); - - ConfigureWarnings(optionsBuilder); - - npgsqlOptionsAction?.Invoke(new NpgsqlDbContextOptionsBuilder(optionsBuilder)); - - return optionsBuilder; - } - - /// - /// - /// Configures the context to connect to a PostgreSQL server with Npgsql, but without initially setting any - /// , or connection string. - /// - /// - /// The connection, data source or connection string must be set explicitly or registered in the DI - /// before the is used to connect to a database. - /// Set a connection using , a data source using - /// , or a connection string using - /// . - /// - /// - /// The builder being used to configure the context. - /// An optional action to allow additional Npgsql-specific configuration. - /// The options builder so that further configuration can be chained. - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - Action? npgsqlOptionsAction = null) - where TContext : DbContext - => (DbContextOptionsBuilder)UseNpgsql( - (DbContextOptionsBuilder)optionsBuilder, npgsqlOptionsAction); - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// A builder for setting options on the context. - /// The connection string of the database to connect to. - /// An optional action to allow additional Npgsql-configuration. - /// - /// The options builder so that further configuration can be chained. - /// - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - string? connectionString, - Action? npgsqlOptionsAction = null) - where TContext : DbContext - => (DbContextOptionsBuilder)UseNpgsql( - (DbContextOptionsBuilder)optionsBuilder, connectionString, npgsqlOptionsAction); - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// A builder for setting options on the context. - /// - /// An existing to be used to connect to the database. If the connection is - /// in the open state then EF will not open or close the connection. If the connection is in the closed - /// state then EF will open and close the connection as needed. The caller owns the connection and is - /// responsible for its disposal. - /// - /// An optional action to allow additional Npgsql-specific configuration. - /// - /// The options builder so that further configuration can be chained. - /// - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - DbConnection connection, - Action? npgsqlOptionsAction = null) - where TContext : DbContext - => (DbContextOptionsBuilder)UseNpgsql( - (DbContextOptionsBuilder)optionsBuilder, connection, npgsqlOptionsAction); - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// The type of context to be configured. - /// The builder being used to configure the context. - /// - /// An existing to be used to connect to the database. If the connection is - /// in the open state then EF will not open or close the connection. If the connection is in the closed - /// state then EF will open and close the connection as needed. - /// - /// - /// If , then EF will take ownership of the connection and will - /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still - /// owns the connection and is responsible for its disposal. - /// - /// An optional action to allow additional Npgsql-specific configuration. - /// The options builder so that further configuration can be chained. - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - DbConnection connection, - bool contextOwnsConnection, - Action? npgsqlOptionsAction = null) - where TContext : DbContext - => (DbContextOptionsBuilder)UseNpgsql( - (DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, npgsqlOptionsAction); - - /// - /// Configures the context to connect to a PostgreSQL database with Npgsql. - /// - /// A builder for setting options on the context. - /// A which will be used to get database connections. - /// An optional action to allow additional Npgsql-specific configuration. - /// - /// The options builder so that further configuration can be chained. - /// - public static DbContextOptionsBuilder UseNpgsql( - this DbContextOptionsBuilder optionsBuilder, - DbDataSource dataSource, - Action? npgsqlOptionsAction = null) - where TContext : DbContext - => (DbContextOptionsBuilder)UseNpgsql( - (DbContextOptionsBuilder)optionsBuilder, dataSource, npgsqlOptionsAction); - - /// - /// Returns an existing instance of , or a new instance if one does not exist. - /// - /// The to search. - /// - /// An existing instance of , or a new instance if one does not exist. - /// - private static NpgsqlOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.Options.FindExtension() is { } existing - ? new NpgsqlOptionsExtension(existing) - : new NpgsqlOptionsExtension(); - - private static void ConfigureWarnings(DbContextOptionsBuilder optionsBuilder) - { - var coreOptionsExtension = optionsBuilder.Options.FindExtension() - ?? new CoreOptionsExtension(); - - coreOptionsExtension = RelationalOptionsExtension.WithDefaultWarningConfiguration(coreOptionsExtension); - - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(coreOptionsExtension); - } -} diff --git a/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs deleted file mode 100644 index 35ac4667bd..0000000000 --- a/src/EFCore.PG/Extensions/NpgsqlMigrationBuilderExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ReSharper disable once CheckNamespace - -namespace Microsoft.EntityFrameworkCore; - -/// -/// 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 NpgsqlMigrationBuilderExtensions -{ - /// - /// Returns true if the active provider in a migration is the Npgsql provider. - /// - /// The migrationBuilder from the parameters on - /// - /// or - /// - /// . - /// True if Npgsql is being used; false otherwise. - public static bool IsNpgsql(this MigrationBuilder builder) - => builder.ActiveProvider == typeof(NpgsqlMigrationBuilderExtensions).GetTypeInfo().Assembly.GetName().Name; - - /// - /// 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 MigrationBuilder EnsurePostgresExtension( - this MigrationBuilder builder, - string name, - string? schema = null, - string? version = null) - { - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NullButNotEmpty(version, nameof(schema)); - - var op = new AlterDatabaseOperation(); - op.GetOrAddPostgresExtension(schema, name, version); - builder.Operations.Add(op); - - return builder; - } -} diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs deleted file mode 100644 index 2cd339ca02..0000000000 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ /dev/null @@ -1,133 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Provides extension methods to configure Entity Framework Core for Npgsql. -/// -// ReSharper disable once UnusedMember.Global -public static class NpgsqlServiceCollectionExtensions -{ - /// - /// - /// Registers the given Entity Framework context as a service in the - /// and configures it to connect to a PostgreSQL database. - /// - /// - /// Use this method when using dependency injection in your application, such as with ASP.NET Core. - /// For applications that don't use dependency injection, consider creating - /// instances directly with its constructor. The method can then be - /// overridden to configure the SQL Server provider and connection string. - /// - /// - /// To configure the for the context, either override the - /// method in your derived context, or supply - /// an optional action to configure the for the context. - /// - /// - /// For more information on how to use this method, see the Entity Framework Core documentation at https://aka.ms/efdocs. - /// For more information on using dependency injection, see https://go.microsoft.com/fwlink/?LinkId=526890. - /// - /// - /// The type of context to be registered. - /// The to add services to. - /// The connection string of the database to connect to. - /// An optional action to allow additional SQL Server specific configuration. - /// An optional action to configure the for the context. - /// The same service collection so that multiple calls can be chained. - public static IServiceCollection AddNpgsql( - this IServiceCollection serviceCollection, - string? connectionString, - Action? npgsqlOptionsAction = null, - Action? optionsAction = null) - where TContext : DbContext - { - Check.NotNull(serviceCollection, nameof(serviceCollection)); - - return serviceCollection.AddDbContext( - (_, options) => - { - optionsAction?.Invoke(options); - options.UseNpgsql(connectionString, npgsqlOptionsAction); - }); - } - - /// - /// - /// Adds the services required by the Npgsql database provider for Entity Framework - /// to an . - /// - /// - /// Calling this method is no longer necessary when building most applications, including those that - /// use dependency injection in ASP.NET or elsewhere. - /// It is only needed when building the internal service provider for use with - /// the method. - /// This is not recommend other than for some advanced scenarios. - /// - /// - /// The to add services to. - /// - /// The same service collection so that multiple calls can be chained. - /// - public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollection serviceCollection) - { - Check.NotNull(serviceCollection, nameof(serviceCollection)); - - new EntityFrameworkNpgsqlServicesBuilder(serviceCollection) - .TryAdd() - .TryAdd>() - .TryAdd(p => p.GetRequiredService()) - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd(p => p.GetRequiredService()) - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd() - .TryAdd(p => p.GetRequiredService()) - .TryAdd() - .TryAddProviderSpecificServices( - b => b - .TryAddSingleton() - .TryAddSingleton() - .TryAddSingleton() - .TryAddSingleton() - .TryAddScoped()) - .TryAddCoreServices(); - - return serviceCollection; - } -} diff --git a/src/EFCore.PG/Extensions/RelationalTypeMappingExtensions.cs b/src/EFCore.PG/Extensions/RelationalTypeMappingExtensions.cs deleted file mode 100644 index 90d72a34c6..0000000000 --- a/src/EFCore.PG/Extensions/RelationalTypeMappingExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore.Storage; - -internal static class RelationalTypeMappingExtensions -{ - internal static string GenerateEmbeddedSqlLiteral(this RelationalTypeMapping mapping, object? value) - => mapping is NpgsqlTypeMapping npgsqlTypeMapping - ? npgsqlTypeMapping.GenerateEmbeddedSqlLiteral(value) - : mapping.GenerateSqlLiteral(value); -} diff --git a/src/EFCore.PG/Extensions/TypeExtensions.cs b/src/EFCore.PG/Extensions/TypeExtensions.cs deleted file mode 100644 index afca6a6617..0000000000 --- a/src/EFCore.PG/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -// ReSharper disable once CheckNamespace -namespace System.Reflection; - -internal static class TypeExtensions -{ - internal static bool IsGenericList(this Type? type) - => type is not null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); - - internal static bool IsArrayOrGenericList(this Type type) - => type.IsArray || type.IsGenericList(); - - internal static bool TryGetElementType(this Type type, [NotNullWhen(true)] out Type? elementType) - { - elementType = type.IsArray - ? type.GetElementType() - : type.IsGenericList() - ? type.GetGenericArguments()[0] - : null; - return elementType is not null; - } - - internal static bool IsRange(this SqlExpression expression) - => expression.TypeMapping is NpgsqlRangeTypeMapping || expression.Type.IsRange(); - - internal static bool IsRange(this Type type) - => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>) - || type is { Name: "Interval" or "DateInterval", Namespace: "NodaTime" }; - - internal static bool IsMultirange(this SqlExpression expression) - => expression.TypeMapping is NpgsqlMultirangeTypeMapping - || expression.Type.IsMultirange(); - - internal static bool IsMultirange(this Type type) - => type.TryGetElementType(out var elementType) && elementType.IsRange(); - - public static PropertyInfo? FindIndexerProperty(this Type type) - { - var defaultPropertyAttribute = type.GetCustomAttributes().FirstOrDefault(); - - return defaultPropertyAttribute is null - ? null - : type.GetRuntimeProperties() - .FirstOrDefault(pi => pi.Name == defaultPropertyAttribute.MemberName && pi.GetIndexParameters().Length == 1); - } -} diff --git a/src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs b/src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs deleted file mode 100644 index d6f25b7649..0000000000 --- a/src/EFCore.PG/Infrastructure/EntityFrameworkNpgsqlServicesBuilder.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 6a76b1dee2..0000000000 --- a/src/EFCore.PG/Infrastructure/INpgsqlDataSourceConfigurationPlugin.cs +++ /dev/null @@ -1,24 +0,0 @@ -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/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs deleted file mode 100644 index 0f08a215fe..0000000000 --- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -/// -/// Represents options for Npgsql that can only be set at the singleton level. -/// -public interface INpgsqlSingletonOptions : ISingletonOptions -{ - /// - /// The backend version to target. - /// - Version PostgresVersion { get; } - - /// - /// Whether the user has explicitly set the backend version to target. - /// - bool IsPostgresVersionSet { get; } - - /// - /// Whether to target Redshift. - /// - bool UseRedshift { get; } - - /// - /// Whether reverse null ordering is enabled. - /// - bool ReverseNullOrderingEnabled { get; } - - /// - /// The collection of enum mappings. - /// - IReadOnlyList EnumDefinitions { get; } - - /// - /// The collection of range mappings. - /// - IReadOnlyList UserRangeDefinitions { get; } -} diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs deleted file mode 100644 index 6942a4f1e1..0000000000 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlModelValidator.cs +++ /dev/null @@ -1,271 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -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 NpgsqlModelValidator : RelationalModelValidator -{ - /// - /// The backend version to target. - /// - private readonly Version _postgresVersion; - - /// - /// 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 NpgsqlModelValidator( - ModelValidatorDependencies dependencies, - RelationalModelValidatorDependencies relationalDependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies, relationalDependencies) - { - _postgresVersion = npgsqlSingletonOptions.PostgresVersion; - } - - /// - /// 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 Validate(IModel model, IDiagnosticsLogger logger) - { - base.Validate(model, logger); - - ValidateIdentityVersionCompatibility(model); - ValidateIndexIncludeProperties(model); - } - - /// - /// Validates that identity columns are used only with PostgreSQL 10.0 or later. - /// - /// The model to validate. - protected virtual void ValidateIdentityVersionCompatibility(IModel model) - { - if (_postgresVersion.AtLeast(10)) - { - return; - } - - var strategy = model.GetValueGenerationStrategy(); - - if (strategy is NpgsqlValueGenerationStrategy.IdentityAlwaysColumn or NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - { - throw new InvalidOperationException( - $"'{strategy}' requires PostgreSQL 10.0 or later. " - + "If you're using an older version, set PostgreSQL compatibility mode by calling " - + $"'optionsBuilder.{nameof(NpgsqlDbContextOptionsBuilder.SetPostgresVersion)}()' in your model's OnConfiguring. " - + "See the docs for more info."); - } - - foreach (var property in model.GetEntityTypes().SelectMany(e => e.GetProperties())) - { - var propertyStrategy = property.GetValueGenerationStrategy(); - - if (propertyStrategy is NpgsqlValueGenerationStrategy.IdentityAlwaysColumn - or NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - { - throw new InvalidOperationException( - $"{property.DeclaringType}.{property.Name}: '{propertyStrategy}' requires PostgreSQL 10.0 or later."); - } - } - } - - /// - /// 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 void ValidateValueGeneration( - IEntityType entityType, - IKey key, - IDiagnosticsLogger logger) - { - if (entityType.GetTableName() != null - && (string?)entityType[RelationalAnnotationNames.MappingStrategy] == RelationalAnnotationNames.TpcMappingStrategy) - { - foreach (var storeGeneratedProperty in key.Properties.Where( - p => (p.ValueGenerated & ValueGenerated.OnAdd) != 0 - && p.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.Sequence)) - { - logger.TpcStoreGeneratedIdentityWarning(storeGeneratedProperty); - } - } - } - - /// - 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 - /// 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 void ValidateIndexIncludeProperties(IModel model) - { - foreach (var index in model.GetEntityTypes().SelectMany(t => t.GetDeclaredIndexes())) - { - var includeProperties = index.GetIncludeProperties(); - if (includeProperties?.Count > 0) - { - var notFound = includeProperties - .FirstOrDefault(i => index.DeclaringEntityType.FindProperty(i) is null); - - if (notFound is not null) - { - throw new InvalidOperationException( - NpgsqlStrings.IncludePropertyNotFound(index.DeclaringEntityType.DisplayName(), notFound)); - } - - var duplicate = includeProperties - .GroupBy(i => i) - .Where(g => g.Count() > 1) - .Select(y => y.Key) - .FirstOrDefault(); - - if (duplicate is not null) - { - throw new InvalidOperationException( - NpgsqlStrings.IncludePropertyDuplicated(index.DeclaringEntityType.DisplayName(), duplicate)); - } - - var inIndex = includeProperties - .FirstOrDefault(i => index.Properties.Any(p => i == p.Name)); - - if (inIndex is not null) - { - throw new InvalidOperationException( - NpgsqlStrings.IncludePropertyInIndex(index.DeclaringEntityType.DisplayName(), inIndex)); - } - } - } - } - - /// - protected override void ValidateStoredProcedures( - IModel model, - IDiagnosticsLogger logger) - { - base.ValidateStoredProcedures(model, logger); - - foreach (var entityType in model.GetEntityTypes()) - { - if (entityType.GetDeleteStoredProcedure() is { } deleteStoredProcedure) - { - ValidateSproc(deleteStoredProcedure, logger); - } - - if (entityType.GetInsertStoredProcedure() is { } insertStoredProcedure) - { - ValidateSproc(insertStoredProcedure, logger); - } - - if (entityType.GetUpdateStoredProcedure() is { } updateStoredProcedure) - { - ValidateSproc(updateStoredProcedure, logger); - } - } - - static void ValidateSproc(IStoredProcedure sproc, IDiagnosticsLogger logger) - { - var entityType = sproc.EntityType; - var storeObjectIdentifier = sproc.GetStoreIdentifier(); - - if (sproc.ResultColumns.Any()) - { - throw new InvalidOperationException( - NpgsqlStrings.StoredProcedureResultColumnsNotSupported( - entityType.DisplayName(), - storeObjectIdentifier.DisplayName())); - } - - if (sproc.IsRowsAffectedReturned) - { - throw new InvalidOperationException( - NpgsqlStrings.StoredProcedureReturnValueNotSupported( - entityType.DisplayName(), - storeObjectIdentifier.DisplayName())); - } - } - } - - /// - protected override void ValidateCompatible( - IProperty property, - IProperty duplicateProperty, - string columnName, - in StoreObjectIdentifier storeObject, - IDiagnosticsLogger logger) - { - base.ValidateCompatible(property, duplicateProperty, columnName, storeObject, logger); - - if (property.GetCompressionMethod(storeObject) != duplicateProperty.GetCompressionMethod(storeObject)) - { - throw new InvalidOperationException( - NpgsqlStrings.DuplicateColumnCompressionMethodMismatch( - duplicateProperty.DeclaringType.DisplayName(), - duplicateProperty.Name, - property.DeclaringType.DisplayName(), - property.Name, - columnName, - storeObject.DisplayName())); - } - } -} diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs deleted file mode 100644 index db0796f2d4..0000000000 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ /dev/null @@ -1,583 +0,0 @@ -using System.Data.Common; -using System.Globalization; -using System.Net.Security; -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -/// -/// Represents options managed by the Npgsql. -/// -public class NpgsqlOptionsExtension : RelationalOptionsExtension -{ - private DbContextOptionsExtensionInfo? _info; - private ParameterTranslationMode? _parameterizedCollectionMode; - - private readonly List _userRangeDefinitions; - private readonly List _enumDefinitions; - private Version? _postgresVersion; - - // We override ParameterizedCollectionMode to set Parameter as the default instead of MultipleParameters, - // which is the EF relational default. In PostgreSQL using native array parameters is better, and the - // query plan problem can be mitigated by telling PostgreSQL to use a custom plan (see #3269). - - /// - /// 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 ParameterTranslationMode ParameterizedCollectionMode - => _parameterizedCollectionMode ?? ParameterTranslationMode.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. - /// - public static readonly Version DefaultPostgresVersion = new(14, 0); - - /// - /// The backend version to target. - /// - public virtual Version PostgresVersion - => _postgresVersion ?? DefaultPostgresVersion; - - /// - /// The backend version to target, but returns unless the user explicitly specified a version. - /// - 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 . - /// - public virtual DbDataSource? DataSource { get; private set; } - - /// - /// The name of the database for administrative operations. - /// - public virtual string? AdminDatabase { get; private set; } - - /// - /// Whether to target Redshift. - /// - public virtual bool UseRedshift { get; private set; } - - /// - /// The list of range mappings specified by the user. - /// - public virtual IReadOnlyList UserRangeDefinitions - => _userRangeDefinitions; - - /// - /// The list of range mappings specified by the user. - /// - public virtual IReadOnlyList EnumDefinitions - => _enumDefinitions; - - /// - /// The specified . - /// - public virtual ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; private set; } - - /// - /// The specified . - /// - public virtual RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; private set; } - - /// - /// The specified . - /// -#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete - public virtual ProvidePasswordCallback? ProvidePasswordCallback { get; private set; } -#pragma warning restore CS0618 - - /// - /// True if reverse null ordering is enabled; otherwise, false. - /// - public virtual bool ReverseNullOrdering { get; private set; } - - /// - /// Initializes an instance of with the default settings. - /// - public NpgsqlOptionsExtension() - { - _userRangeDefinitions = []; - _enumDefinitions = []; - } - - // NB: When adding new options, make sure to update the copy ctor below. - /// - /// Initializes an instance of by copying the specified instance. - /// - /// The instance to copy. - public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom) - : base(copyFrom) - { - DataSource = copyFrom.DataSource; - DataSourceBuilderAction = copyFrom.DataSourceBuilderAction; - AdminDatabase = copyFrom.AdminDatabase; - _postgresVersion = copyFrom._postgresVersion; - UseRedshift = copyFrom.UseRedshift; - _userRangeDefinitions = [..copyFrom._userRangeDefinitions]; - _enumDefinitions = [..copyFrom._enumDefinitions]; - ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback; - RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback; - ProvidePasswordCallback = copyFrom.ProvidePasswordCallback; - ReverseNullOrdering = copyFrom.ReverseNullOrdering; - } - - // The following is a hack to set the default minimum batch size to 2 in Npgsql - // See https://github.com/aspnet/EntityFrameworkCore/pull/10091 - /// - /// 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 int? MinBatchSize - => base.MinBatchSize ?? 2; - - // We need to override WithUseParameterizedCollectionMode since we override ParameterizedCollectionMode above - /// - public override RelationalOptionsExtension WithUseParameterizedCollectionMode(ParameterTranslationMode parameterizedCollectionMode) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone._parameterizedCollectionMode = parameterizedCollectionMode; - - 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 . - /// - /// 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 . - /// - /// The option to change. - /// A new instance with the option changed. - public virtual RelationalOptionsExtension WithDataSource(DbDataSource? dataSource) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.DataSource = dataSource; - - return clone; - } - - /// - public override RelationalOptionsExtension WithConnectionString(string? connectionString) - { - var clone = (NpgsqlOptionsExtension)base.WithConnectionString(connectionString); - - clone.DataSource = null; - - return clone; - } - - /// - public override RelationalOptionsExtension WithConnection(DbConnection? connection) - { - var clone = (NpgsqlOptionsExtension)base.WithConnection(connection); - - clone.DataSource = null; - - return clone; - } - - /// - /// Returns a copy of the current instance configured with the specified range mapping. - /// - public virtual NpgsqlOptionsExtension WithUserRangeDefinition( - string rangeName, - string? schemaName = null, - string? subtypeName = null) - => WithUserRangeDefinition(rangeName, schemaName, typeof(TSubtype), subtypeName); - - /// - /// Returns a copy of the current instance configured with the specified range mapping. - /// - public virtual NpgsqlOptionsExtension WithUserRangeDefinition( - string rangeName, - string? schemaName, - Type subtypeClrType, - string? subtypeName) - { - Check.NotEmpty(rangeName, nameof(rangeName)); - Check.NotNull(subtypeClrType, nameof(subtypeClrType)); - - var clone = (NpgsqlOptionsExtension)Clone(); - - clone._userRangeDefinitions.Add(new UserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName)); - - 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. - /// - /// The name of the database for administrative operations. - public virtual NpgsqlOptionsExtension WithAdminDatabase(string? adminDatabase) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.AdminDatabase = adminDatabase; - - return clone; - } - - /// - /// Returns a copy of the current instance with the specified PostgreSQL version. - /// - /// The backend version to target. - /// - /// A copy of the current instance with the specified PostgreSQL version. - /// - public virtual NpgsqlOptionsExtension WithPostgresVersion(Version? postgresVersion) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone._postgresVersion = postgresVersion; - - return clone; - } - - /// - /// Returns a copy of the current instance with the specified Redshift settings. - /// - /// Whether to target Redshift. - /// - /// A copy of the current instance with the specified Redshift setting. - /// - public virtual NpgsqlOptionsExtension WithRedshift(bool useRedshift) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.UseRedshift = useRedshift; - - return clone; - } - - /// - /// Returns a copy of the current instance configured with the specified value.. - /// - /// True to enable reverse null ordering; otherwise, false. - internal virtual NpgsqlOptionsExtension WithReverseNullOrdering(bool reverseNullOrdering) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.ReverseNullOrdering = reverseNullOrdering; - - return clone; - } - - /// - public override void Validate(IDbContextOptions options) - { - base.Validate(options); - - // If we don't have an explicitly-configured data source, try to get one from the application service provider. - var dataSource = DataSource - ?? options.FindExtension()?.ApplicationServiceProvider?.GetService(); - - if (dataSource is not null) - { - if (ProvideClientCertificatesCallback is not null) - { - throw new InvalidOperationException( - "When passing an NpgsqlDataSource to UseNpgsql(), call 'ProvideClientCertificatesCallback' on NpgsqlDataSourceBuilder rather than in UseNpgsql()."); - } - - if (RemoteCertificateValidationCallback is not null) - { - throw new InvalidOperationException( - "When passing an NpgsqlDataSource to UseNpgsql(), call 'RemoteCertificateValidationCallback' on NpgsqlDataSourceBuilder rather than in UseNpgsql()."); - } - - if (ProvidePasswordCallback is not null) - { - throw new InvalidOperationException( - "When passing an NpgsqlDataSource to UseNpgsql(), 'ProviderPasswordCallback' cannot be used in UseNpgsql(). See https://www.npgsql.org/doc/security.html for configuring passwords and token rotation on NpgsqlDataSourceBuilder."); - } - } - - if (UseRedshift && _postgresVersion is not null) - { - throw new InvalidOperationException($"{nameof(UseRedshift)} and {nameof(PostgresVersion)} cannot both be set"); - } - } - - #region Authentication - - /// - /// Returns a copy of the current instance with the specified . - /// - /// The specified callback. - public virtual NpgsqlOptionsExtension WithProvideClientCertificatesCallback(ProvideClientCertificatesCallback? callback) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.ProvideClientCertificatesCallback = callback; - - return clone; - } - - /// - /// Returns a copy of the current instance with the specified . - /// - /// The specified callback. - public virtual NpgsqlOptionsExtension WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback? callback) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.RemoteCertificateValidationCallback = callback; - - return clone; - } - - /// - /// Returns a copy of the current instance with the specified . - /// - /// The specified callback. -#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete - public virtual NpgsqlOptionsExtension WithProvidePasswordCallback(ProvidePasswordCallback? callback) - { - var clone = (NpgsqlOptionsExtension)Clone(); - - clone.ProvidePasswordCallback = callback; - - return clone; - } -#pragma warning restore CS0618 - - #endregion Authentication - - #region Infrastructure - - /// - protected override RelationalOptionsExtension Clone() - => new NpgsqlOptionsExtension(this); - - /// - public override void ApplyServices(IServiceCollection services) - => services.AddEntityFrameworkNpgsql(); - - /// - public override DbContextOptionsExtensionInfo Info - => _info ??= new ExtensionInfo(this); - - private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : RelationalExtensionInfo(extension) - { - private int? _serviceProviderHash; - private string? _logFragment; - - private new NpgsqlOptionsExtension Extension - => (NpgsqlOptionsExtension)base.Extension; - - public override bool IsDatabaseProvider - => true; - - public override string LogFragment - { - get - { - if (_logFragment is not null) - { - return _logFragment; - } - - var builder = new StringBuilder(base.LogFragment); - - if (Extension.AdminDatabase is not null) - { - builder.Append(nameof(Extension.AdminDatabase)).Append("=").Append(Extension.AdminDatabase).Append(' '); - } - - if (Extension._postgresVersion is not null) - { - builder.Append(nameof(Extension.PostgresVersion)).Append("=").Append(Extension.PostgresVersion).Append(' '); - } - - if (Extension.UseRedshift) - { - builder.Append(nameof(Extension.UseRedshift)).Append(' '); - } - - if (Extension.ProvideClientCertificatesCallback is not null) - { - builder.Append(nameof(Extension.ProvideClientCertificatesCallback)).Append(" "); - } - - if (Extension.RemoteCertificateValidationCallback is not null) - { - builder.Append(nameof(Extension.RemoteCertificateValidationCallback)).Append(" "); - } - - if (Extension.ProvidePasswordCallback is not null) - { - builder.Append(nameof(Extension.ProvidePasswordCallback)).Append(" "); - } - - if (Extension.ReverseNullOrdering) - { - builder.Append(nameof(Extension.ReverseNullOrdering)).Append(" "); - } - - if (Extension.UserRangeDefinitions.Count > 0) - { - builder.Append(nameof(Extension.UserRangeDefinitions)).Append("=["); - foreach (var item in Extension.UserRangeDefinitions) - { - builder.Append(item.SubtypeClrType).Append("=>"); - - if (item.StoreTypeSchema is not null) - { - builder.Append(item.StoreTypeSchema).Append("."); - } - - builder.Append(item.StoreTypeName); - - if (item.SubtypeName is not null) - { - builder.Append("(").Append(item.SubtypeName).Append(")"); - } - - builder.Append(";"); - } - - builder.Length -= 1; - builder.Append("] "); - } - - return _logFragment = builder.ToString(); - } - } - - public override int GetServiceProviderHashCode() - { - if (_serviceProviderHash is null) - { - var hashCode = new HashCode(); - - foreach (var userRangeDefinition in Extension._userRangeDefinitions) - { - hashCode.Add(userRangeDefinition); - } - - hashCode.Add(Extension.AdminDatabase); - hashCode.Add(Extension.PostgresVersion); - hashCode.Add(Extension.UseRedshift); - hashCode.Add(Extension.ProvideClientCertificatesCallback); - hashCode.Add(Extension.RemoteCertificateValidationCallback); - hashCode.Add(Extension.ProvidePasswordCallback); - hashCode.Add(Extension.ReverseNullOrdering); - - _serviceProviderHash = hashCode.ToHashCode(); - } - - return _serviceProviderHash.Value; - } - - public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) - => other is ExtensionInfo otherInfo - && Extension.PostgresVersion == otherInfo.Extension.PostgresVersion - && Extension.ReverseNullOrdering == otherInfo.Extension.ReverseNullOrdering - && Extension.EnumDefinitions.SequenceEqual(otherInfo.Extension.EnumDefinitions) - && Extension.UserRangeDefinitions.SequenceEqual(otherInfo.Extension.UserRangeDefinitions) - && Extension.UseRedshift == otherInfo.Extension.UseRedshift; - - /// - public override void PopulateDebugInfo(IDictionary debugInfo) - { - debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.UseAdminDatabase)] - = (Extension.AdminDatabase?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); - - debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.SetPostgresVersion)] - = Extension.PostgresVersion.GetHashCode().ToString(CultureInfo.InvariantCulture); - - debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.UseRedshift)] - = Extension.UseRedshift.GetHashCode().ToString(CultureInfo.InvariantCulture); - - debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.ReverseNullOrdering)] - = Extension.ReverseNullOrdering.GetHashCode().ToString(CultureInfo.InvariantCulture); - - debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.RemoteCertificateValidationCallback)] - = (Extension.RemoteCertificateValidationCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); - - debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.ProvideClientCertificatesCallback)] - = (Extension.ProvideClientCertificatesCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture); - - 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[ - "Npgsql.EntityFrameworkCore.PostgreSQL:" - + nameof(NpgsqlDbContextOptionsBuilder.MapRange) - + ":" - + rangeDefinition.SubtypeClrType.Name] - = rangeDefinition.GetHashCode().ToString(CultureInfo.InvariantCulture); - } - } - } - - #endregion Infrastructure -} diff --git a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs deleted file mode 100644 index aedcc83a0b..0000000000 --- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System.Net.Security; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -/// -/// Allows for options specific to PostgreSQL to be configured for a . -/// -public class NpgsqlDbContextOptionsBuilder - : RelationalDbContextOptionsBuilder -{ - /// - /// Initializes a new instance of the class. - /// - /// The core options builder. - public NpgsqlDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) - : base(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). - /// - /// The name of the database for administrative operations. - public virtual NpgsqlDbContextOptionsBuilder UseAdminDatabase(string? dbName) - => WithOption(e => e.WithAdminDatabase(dbName)); - - /// - /// Configures the backend version to target. - /// - /// The backend version to target. - public virtual NpgsqlDbContextOptionsBuilder SetPostgresVersion(Version? postgresVersion) - => WithOption(e => e.WithPostgresVersion(postgresVersion)); - - /// - /// Configures the backend version to target. - /// - /// The PostgreSQL major version to target. - /// The PostgreSQL minor version to target. - public virtual NpgsqlDbContextOptionsBuilder SetPostgresVersion(int major, int minor) - => SetPostgresVersion(new Version(major, minor)); - - /// - /// Configures the provider to work in Redshift compatibility mode, which avoids certain unsupported features from modern - /// PostgreSQL versions. - /// - /// Whether to target Redshift. - public virtual NpgsqlDbContextOptionsBuilder UseRedshift(bool useRedshift = true) - => WithOption(e => e.WithRedshift(useRedshift)); - - #region MapRange - - /// - /// Maps a user-defined PostgreSQL range type for use. - /// - /// - /// The CLR type of the range's subtype (or element). - /// The actual mapped type will be an over this type. - /// - /// The name of the PostgreSQL range type to be mapped. - /// The name of the PostgreSQL schema in which the range is defined. - /// - /// Optionally, the name of the range's PostgreSQL subtype (or element). - /// This is usually not needed - the subtype will be inferred based on . - /// - /// - /// To map a range of PostgreSQL real, use the following: - /// NpgsqlTypeMappingSource.MapRange{float}("floatrange"); - /// - public virtual NpgsqlDbContextOptionsBuilder MapRange( - string rangeName, - string? schemaName = null, - string? subtypeName = null) - => MapRange(rangeName, typeof(TSubtype), schemaName, subtypeName); - - /// - /// Maps a user-defined PostgreSQL range type for use. - /// - /// The name of the PostgreSQL range type to be mapped. - /// The name of the PostgreSQL schema in which the range is defined. - /// - /// The CLR type of the range's subtype (or element). - /// The actual mapped type will be an over this type. - /// - /// - /// Optionally, the name of the range's PostgreSQL subtype (or element). - /// This is usually not needed - the subtype will be inferred based on . - /// - /// - /// To map a range of PostgreSQL real, use the following: - /// NpgsqlTypeMappingSource.MapRange("floatrange", typeof(float)); - /// - public virtual NpgsqlDbContextOptionsBuilder MapRange( - string rangeName, - Type subtypeClrType, - string? schemaName = null, - 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 enum 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 enum 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 - /// accordingly, and since this isn't done this feature isn't publicly exposed. - /// - /// True to enable reverse null ordering; otherwise, false. - internal virtual NpgsqlDbContextOptionsBuilder ReverseNullOrdering(bool reverseNullOrdering = true) - => WithOption(e => e.WithReverseNullOrdering(reverseNullOrdering)); - - #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)); - - /// - /// 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)); - - /// - /// Configures the to use the specified . - /// - /// The callback to use. - [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)); - - #endregion Authentication (obsolete) - - #region Retrying execution strategy - - /// - /// Configures the context to use the default retrying . - /// - /// - /// An instance of configured to use - /// the default retrying . - /// - public virtual NpgsqlDbContextOptionsBuilder EnableRetryOnFailure() - => ExecutionStrategy(c => new NpgsqlRetryingExecutionStrategy(c)); - - /// - /// Configures the context to use the default retrying . - /// - /// - /// An instance of with the specified parameters. - /// - public virtual NpgsqlDbContextOptionsBuilder EnableRetryOnFailure(int maxRetryCount) - => ExecutionStrategy(c => new NpgsqlRetryingExecutionStrategy(c, maxRetryCount)); - - /// - /// Configures the context to use the default retrying . - /// - /// Additional error codes that should be considered transient. - /// - /// An instance of with the specified parameters. - /// - public virtual NpgsqlDbContextOptionsBuilder EnableRetryOnFailure(ICollection? errorCodesToAdd) - => ExecutionStrategy(c => new NpgsqlRetryingExecutionStrategy(c, errorCodesToAdd)); - - /// - /// Configures the context to use the default retrying . - /// - /// The maximum number of retry attempts. - /// The maximum delay between retries. - /// Additional error codes that should be considered transient. - /// - /// An instance of with the specified parameters. - /// - public virtual NpgsqlDbContextOptionsBuilder EnableRetryOnFailure( - int maxRetryCount, - TimeSpan maxRetryDelay, - ICollection? errorCodesToAdd) - => ExecutionStrategy(c => new NpgsqlRetryingExecutionStrategy(c, maxRetryCount, maxRetryDelay, errorCodesToAdd)); - - #endregion Retrying execution strategy -} diff --git a/src/EFCore.PG/Internal/NpgsqlLoggerExtensions.cs b/src/EFCore.PG/Internal/NpgsqlLoggerExtensions.cs deleted file mode 100644 index fa207d4cd5..0000000000 --- a/src/EFCore.PG/Internal/NpgsqlLoggerExtensions.cs +++ /dev/null @@ -1,239 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlLoggerExtensions -{ - /// - /// 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 void MissingSchemaWarning( - this IDiagnosticsLogger diagnostics, - string? schemaName) - { - var definition = NpgsqlResources.LogMissingSchema(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, schemaName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void MissingTableWarning( - this IDiagnosticsLogger diagnostics, - string? tableName) - { - var definition = NpgsqlResources.LogMissingTable(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, tableName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void ForeignKeyReferencesMissingPrincipalTableWarning( - this IDiagnosticsLogger diagnostics, - string? foreignKeyName, - string? tableName, - string? principalTableName) - { - var definition = NpgsqlResources.LogPrincipalTableNotInSelectionSet(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, foreignKeyName, tableName, principalTableName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void ColumnFound( - this IDiagnosticsLogger diagnostics, - string tableName, - string columnName, - string dataTypeName, - bool nullable, - bool identity, - string? defaultValue, - string? computedValue) - { - var definition = NpgsqlResources.LogFoundColumn(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log( - diagnostics, - l => l.LogDebug( - definition.EventId, - null, - definition.MessageFormat, - tableName, - columnName, - dataTypeName, - nullable, - identity, - defaultValue, - computedValue)); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void CollationFound( - this IDiagnosticsLogger diagnostics, - string schema, - string collationName, - string lcCollate, - string lcCtype, - string? provider, - bool deterministic) - { - var definition = NpgsqlResources.LogFoundCollation(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, collationName, schema, lcCollate, lcCtype, provider, deterministic); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void UniqueConstraintFound( - this IDiagnosticsLogger diagnostics, - string? uniqueConstraintName, - string tableName) - { - var definition = NpgsqlResources.LogFoundUniqueConstraint(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, uniqueConstraintName, tableName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void EnumColumnSkippedWarning( - this IDiagnosticsLogger diagnostics, - string columnName) - { - var definition = NpgsqlResources.LogEnumColumnSkipped(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, columnName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void ExpressionIndexSkippedWarning( - this IDiagnosticsLogger diagnostics, - string indexName, - string tableName) - { - var definition = NpgsqlResources.LogExpressionIndexSkipped(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, indexName, tableName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void UnsupportedColumnIndexSkippedWarning( - this IDiagnosticsLogger diagnostics, - string indexName, - string tableName) - { - var definition = NpgsqlResources.LogUnsupportedColumnIndexSkipped(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, indexName, tableName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } - - /// - /// 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 void UnsupportedColumnConstraintSkippedWarning( - this IDiagnosticsLogger diagnostics, - string? indexName, - string tableName) - { - var definition = NpgsqlResources.LogUnsupportedColumnConstraintSkipped(diagnostics); - - if (diagnostics.ShouldLog(definition)) - { - definition.Log(diagnostics, indexName, tableName); - } - - // No DiagnosticsSource events because these are purely design-time messages - } -} diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs deleted file mode 100644 index 5adba7a0c9..0000000000 --- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlSingletonOptions : INpgsqlSingletonOptions -{ - /// - public virtual Version PostgresVersion { get; private set; } = 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 virtual bool IsPostgresVersionSet { 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 bool UseRedshift { 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 bool ReverseNullOrderingEnabled { 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 IReadOnlyList EnumDefinitions { 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 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 NpgsqlSingletonOptions() - { - EnumDefinitions = []; - UserRangeDefinitions = []; - } - - /// - public virtual void Initialize(IDbContextOptions options) - { - var npgsqlOptions = options.FindExtension() ?? new NpgsqlOptionsExtension(); - - PostgresVersion = npgsqlOptions.PostgresVersion; - IsPostgresVersionSet = npgsqlOptions.IsPostgresVersionSet; - UseRedshift = npgsqlOptions.UseRedshift; - ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering; - EnumDefinitions = npgsqlOptions.EnumDefinitions; - UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions; - } - - /// - public virtual void Validate(IDbContextOptions options) - { - var npgsqlOptions = options.FindExtension() ?? new NpgsqlOptionsExtension(); - - if (PostgresVersion != npgsqlOptions.PostgresVersion) - { - throw new InvalidOperationException( - CoreStrings.SingletonOptionChanged( - nameof(NpgsqlDbContextOptionsBuilder.SetPostgresVersion), - nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } - - if (UseRedshift != npgsqlOptions.UseRedshift) - { - throw new InvalidOperationException( - CoreStrings.SingletonOptionChanged( - nameof(NpgsqlDbContextOptionsBuilder.UseRedshift), - nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } - - if (ReverseNullOrderingEnabled != npgsqlOptions.ReverseNullOrdering) - { - throw new InvalidOperationException( - CoreStrings.SingletonOptionChanged( - nameof(NpgsqlDbContextOptionsBuilder.ReverseNullOrdering), - nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } - - if (!EnumDefinitions.SequenceEqual(npgsqlOptions.EnumDefinitions)) - { - throw new InvalidOperationException( - CoreStrings.SingletonOptionChanged( - nameof(NpgsqlDbContextOptionsBuilder.MapEnum), - nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } - - if (!UserRangeDefinitions.SequenceEqual(npgsqlOptions.UserRangeDefinitions)) - { - throw new InvalidOperationException( - CoreStrings.SingletonOptionChanged( - nameof(NpgsqlDbContextOptionsBuilder.MapRange), - nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } - } -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs deleted file mode 100644 index 7bbdf9ed60..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A builder for building conventions for Npgsql. -/// -/// -/// -/// The service lifetime is and multiple registrations are allowed. This means that each -/// instance will use its own set of instances of this service. The implementations may depend on other -/// services registered with any lifetime. The implementations do not need to be thread-safe. -/// -/// -/// See Model building conventions, and -/// -/// -public class NpgsqlConventionSetBuilder : RelationalConventionSetBuilder -{ - private readonly NpgsqlTypeMappingSource _typeMappingSource; - private readonly Version _postgresVersion; - private readonly IReadOnlyList _enumDefinitions; - - /// - /// Creates a new instance. - /// - /// The core dependencies for this service. - /// The relational dependencies for this service. - /// The type mapping source to use. - /// The singleton options to use. - public NpgsqlConventionSetBuilder( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies, - IRelationalTypeMappingSource typeMappingSource, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies, relationalDependencies) - { - _typeMappingSource = (NpgsqlTypeMappingSource)typeMappingSource; - _postgresVersion = npgsqlSingletonOptions.PostgresVersion; - _enumDefinitions = npgsqlSingletonOptions.EnumDefinitions; - } - - /// - public override ConventionSet CreateConventionSet() - { - var conventionSet = base.CreateConventionSet(); - - var valueGenerationStrategyConvention = - new NpgsqlValueGenerationStrategyConvention(Dependencies, RelationalDependencies, _postgresVersion); - conventionSet.ModelInitializedConventions.Add(valueGenerationStrategyConvention); - conventionSet.ModelInitializedConventions.Add( - new RelationalMaxIdentifierLengthConvention(63, Dependencies, RelationalDependencies)); - - ValueGenerationConvention valueGenerationConvention = new NpgsqlValueGenerationConvention(Dependencies, RelationalDependencies); - ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, valueGenerationConvention); - - ReplaceConvention( - conventionSet.EntityTypeAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); - - ReplaceConvention(conventionSet.EntityTypePrimaryKeyChangedConventions, valueGenerationConvention); - - ReplaceConvention(conventionSet.ForeignKeyAddedConventions, valueGenerationConvention); - - ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention); - - var storeGenerationConvention = - new NpgsqlStoreGenerationConvention(Dependencies, RelationalDependencies); - ReplaceConvention(conventionSet.PropertyAnnotationChangedConventions, storeGenerationConvention); - ReplaceConvention( - conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); - - conventionSet.ModelFinalizingConventions.Add(valueGenerationStrategyConvention); - conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresModelFinalizingConvention(_typeMappingSource, _enumDefinitions)); - ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention); - ReplaceConvention( - conventionSet.ModelFinalizingConventions, - (SharedTableConvention)new NpgsqlSharedTableConvention(Dependencies, RelationalDependencies)); - - ReplaceConvention( - conventionSet.ModelFinalizedConventions, - (RuntimeModelConvention)new NpgsqlRuntimeModelConvention(Dependencies, RelationalDependencies)); - - return conventionSet; - } - - /// - /// - /// Call this method to build a for Npgsql when using - /// the outside of . - /// - /// - /// Note that it is unusual to use this method. - /// Consider using in the normal way instead. - /// - /// - /// The convention set. - public static ConventionSet Build() - { - using var serviceScope = CreateServiceScope(); - using var context = serviceScope.ServiceProvider.GetRequiredService(); - return ConventionSet.CreateConventionSet(context); - } - - /// - /// - /// Call this method to build a for Npgsql outside of . - /// - /// - /// Note that it is unusual to use this method. - /// Consider using in the normal way instead. - /// - /// - /// The convention set. - public static ModelBuilder CreateModelBuilder() - { - using var serviceScope = CreateServiceScope(); - using var context = serviceScope.ServiceProvider.GetRequiredService(); - return new ModelBuilder(ConventionSet.CreateConventionSet(context), context.GetService()); - } - - private static IServiceScope CreateServiceScope() - { - var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() - .AddDbContext( - (p, o) => - o.UseNpgsql("Server=.") - .UseInternalServiceProvider(p)) - .BuildServiceProvider(); - - return serviceProvider.GetRequiredService().CreateScope(); - } -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs deleted file mode 100644 index 92777fe3af..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A convention that discovers certain common PostgreSQL extensions based on store types used in the model (e.g. hstore). -/// -/// -/// See Model building conventions. -/// -public class NpgsqlPostgresModelFinalizingConvention( - NpgsqlTypeMappingSource typeMappingSource, - IReadOnlyList enumDefinitions) : IModelFinalizingConvention -{ - /// - public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) - { - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) - { - foreach (var property in entityType.GetDeclaredProperties()) - { - var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping() - ?? typeMappingSource.FindMapping((IProperty)property); - - if (typeMapping is not null) - { - DiscoverPostgresExtensions(property, typeMapping, modelBuilder); - ProcessRowVersionProperty(property, typeMapping); - } - } - } - - 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()); - } - } - - /// - /// Discovers certain common PostgreSQL extensions based on property store types (e.g. hstore). - /// - protected virtual void DiscoverPostgresExtensions( - IConventionProperty property, - RelationalTypeMapping typeMapping, - IConventionModelBuilder modelBuilder) - { - // TODO: does not work if CREATE EXTENSION was done on a non-default schema. #3177 - switch (typeMapping.StoreType) - { - case "hstore": - modelBuilder.HasPostgresExtension("hstore"); - break; - case "citext": - modelBuilder.HasPostgresExtension("citext"); - break; - case "ltree": - case "lquery": - case "ltxtquery": - modelBuilder.HasPostgresExtension("ltree"); - break; - case "cube": - modelBuilder.HasPostgresExtension("cube"); - break; - } - } - - /// - /// Detects properties which are uint, OnAddOrUpdate and configured as concurrency tokens, and maps these to the PostgreSQL - /// internal "xmin" column, which changes every time the row is modified. - /// - protected virtual void ProcessRowVersionProperty(IConventionProperty property, RelationalTypeMapping typeMapping) - { - if (property is { ValueGenerated: ValueGenerated.OnAddOrUpdate, IsConcurrencyToken: true } - && typeMapping.StoreType == "xid") - { - property.Builder.HasColumnName("xmin"); - } - } -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlRuntimeModelConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlRuntimeModelConvention.cs deleted file mode 100644 index d932b07f0c..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlRuntimeModelConvention.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A convention that creates an optimized copy of the mutable model. -/// -public class NpgsqlRuntimeModelConvention : RelationalRuntimeModelConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - public NpgsqlRuntimeModelConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } - - /// - protected override void ProcessModelAnnotations( - Dictionary annotations, - IModel model, - RuntimeModel runtimeModel, - bool runtime) - { - base.ProcessModelAnnotations(annotations, model, runtimeModel, runtime); - - if (!runtime) - { - annotations.Remove(NpgsqlAnnotationNames.DatabaseTemplate); - annotations.Remove(NpgsqlAnnotationNames.Tablespace); - annotations.Remove(NpgsqlAnnotationNames.CollationDefinitionPrefix); - -#pragma warning disable CS0618 - annotations.Remove(NpgsqlAnnotationNames.DefaultColumnCollation); -#pragma warning restore CS0618 - - foreach (var annotationName in annotations.Keys.Where( - k => - k.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) - || k.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) - || k.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - } - - /// - protected override void ProcessEntityTypeAnnotations( - Dictionary annotations, - IEntityType entityType, - RuntimeEntityType runtimeEntityType, - bool runtime) - { - base.ProcessEntityTypeAnnotations(annotations, entityType, runtimeEntityType, runtime); - - if (!runtime) - { - annotations.Remove(NpgsqlAnnotationNames.UnloggedTable); - - foreach (var annotationName in annotations.Keys.Where( - k => k.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - } - - /// - protected override void ProcessPropertyAnnotations( - Dictionary annotations, - IProperty property, - RuntimeProperty runtimeProperty, - bool runtime) - { - base.ProcessPropertyAnnotations(annotations, property, runtimeProperty, runtime); - - if (!runtime) - { - annotations.Remove(NpgsqlAnnotationNames.IdentityOptions); - annotations.Remove(NpgsqlAnnotationNames.TsVectorConfig); - annotations.Remove(NpgsqlAnnotationNames.TsVectorProperties); - - if (!annotations.ContainsKey(NpgsqlAnnotationNames.ValueGenerationStrategy)) - { - annotations[NpgsqlAnnotationNames.ValueGenerationStrategy] = property.GetValueGenerationStrategy(); - } - } - } - - /// - protected override void ProcessIndexAnnotations( - Dictionary annotations, - IIndex index, - RuntimeIndex runtimeIndex, - bool runtime) - { - base.ProcessIndexAnnotations(annotations, index, runtimeIndex, runtime); - - if (!runtime) - { - annotations.Remove(NpgsqlAnnotationNames.IndexOperators); - annotations.Remove(NpgsqlAnnotationNames.IndexSortOrder); - annotations.Remove(NpgsqlAnnotationNames.IndexNullSortOrder); - annotations.Remove(NpgsqlAnnotationNames.IndexInclude); - annotations.Remove(NpgsqlAnnotationNames.CreatedConcurrently); - annotations.Remove(NpgsqlAnnotationNames.NullsDistinct); - - foreach (var annotationName in annotations.Keys.Where( - k => k.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - annotations.Remove(annotationName); - } - } - } -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlSharedTableConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlSharedTableConvention.cs deleted file mode 100644 index b37f046fd8..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlSharedTableConvention.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A convention that manipulates names of database objects for entity types that share a table to avoid clashes. -/// -public class NpgsqlSharedTableConvention : SharedTableConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - public NpgsqlSharedTableConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } - - /// - protected override bool AreCompatible(IReadOnlyIndex index, IReadOnlyIndex duplicateIndex, in StoreObjectIdentifier storeObject) - => base.AreCompatible(index, duplicateIndex, storeObject) - && index.AreCompatibleForNpgsql(duplicateIndex, storeObject, shouldThrow: false); - - /// - protected override bool KeysUniqueAcrossTables - => true; - - /// - protected override bool ForeignKeysUniqueAcrossTables - => false; - - /// - protected override bool IndexesUniqueAcrossTables - => true; - - /// - protected override bool CheckConstraintsUniqueAcrossTables - => false; -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs deleted file mode 100644 index 33e60efb8e..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlStoreGenerationConvention.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A convention that ensures that properties aren't configured to have a default value, as computed column -/// or using a at the same time. -/// -public class NpgsqlStoreGenerationConvention : StoreGenerationConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - public NpgsqlStoreGenerationConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } - - /// - /// Called after an annotation is changed on a property. - /// - /// The builder for the property. - /// The annotation name. - /// The new annotation. - /// The old annotation. - /// Additional information associated with convention execution. - public override void ProcessPropertyAnnotationChanged( - IConventionPropertyBuilder propertyBuilder, - string name, - IConventionAnnotation? annotation, - IConventionAnnotation? oldAnnotation, - IConventionContext context) - { - if (annotation is null - || oldAnnotation?.Value is not null) - { - return; - } - - var configurationSource = annotation.GetConfigurationSource(); - var fromDataAnnotation = configurationSource != ConfigurationSource.Convention; - switch (name) - { - case RelationalAnnotationNames.DefaultValue: - if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) is null - && propertyBuilder.HasDefaultValue(null, fromDataAnnotation) is not null) - { - context.StopProcessing(); - return; - } - - break; - case RelationalAnnotationNames.DefaultValueSql: - if (propertyBuilder.Metadata.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.Sequence - && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) == null - && propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) != null) - { - context.StopProcessing(); - return; - } - - break; - case RelationalAnnotationNames.ComputedColumnSql: - if (propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) is null - && propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) is not null) - { - context.StopProcessing(); - return; - } - - break; - case NpgsqlAnnotationNames.ValueGenerationStrategy: - if (((propertyBuilder.Metadata.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.Sequence - && (propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null - || propertyBuilder.HasDefaultValueSql(null, fromDataAnnotation) == null - || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)) - || (propertyBuilder.HasDefaultValue(null, fromDataAnnotation) == null - || propertyBuilder.HasComputedColumnSql(null, fromDataAnnotation) == null)) - && propertyBuilder.HasValueGenerationStrategy(null, fromDataAnnotation) != null) - { - context.StopProcessing(); - return; - } - - break; - } - - base.ProcessPropertyAnnotationChanged(propertyBuilder, name, annotation, oldAnnotation, context); - } - - /// - protected override void Validate(IConventionProperty property, in StoreObjectIdentifier storeObject) - { - if (property.GetValueGenerationStrategyConfigurationSource() is not null) - { - var generationStrategy = property.GetValueGenerationStrategy(storeObject); - if (generationStrategy == NpgsqlValueGenerationStrategy.None) - { - base.Validate(property, storeObject); - return; - } - - if (property.TryGetDefaultValue(storeObject, out _)) - { - throw new InvalidOperationException( - RelationalStrings.ConflictingColumnServerGeneration( - "NpgsqlValueGenerationStrategy", property.Name, "DefaultValue")); - } - - if (property.GetDefaultValueSql() is not null) - { - throw new InvalidOperationException( - RelationalStrings.ConflictingColumnServerGeneration( - "NpgsqlValueGenerationStrategy", property.Name, "DefaultValueSql")); - } - - if (property.GetComputedColumnSql() is not null) - { - throw new InvalidOperationException( - RelationalStrings.ConflictingColumnServerGeneration( - "NpgsqlValueGenerationStrategy", property.Name, "ComputedColumnSql")); - } - } - - base.Validate(property, storeObject); - } -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs deleted file mode 100644 index 32b057f81c..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A convention that configures store value generation as on properties that are -/// part of the primary key and not part of any foreign keys, were configured to have a database default value -/// or were configured to use a . -/// It also configures properties as if they were configured as computed columns. -/// -public class NpgsqlValueGenerationConvention : RelationalValueGenerationConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - public NpgsqlValueGenerationConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } - - /// - /// Called after an annotation is changed on a property. - /// - /// The builder for the property. - /// The annotation name. - /// The new annotation. - /// The old annotation. - /// Additional information associated with convention execution. - public override void ProcessPropertyAnnotationChanged( - IConventionPropertyBuilder propertyBuilder, - string name, - IConventionAnnotation? annotation, - IConventionAnnotation? oldAnnotation, - IConventionContext context) - { - if (name == NpgsqlAnnotationNames.ValueGenerationStrategy) - { - propertyBuilder.ValueGenerated(GetValueGenerated(propertyBuilder.Metadata)); - return; - } - - if (name == NpgsqlAnnotationNames.TsVectorConfig && propertyBuilder.Metadata.GetTsVectorConfig() is not null) - { - propertyBuilder.ValueGenerated(ValueGenerated.OnAddOrUpdate); - return; - } - - base.ProcessPropertyAnnotationChanged(propertyBuilder, name, annotation, oldAnnotation, context); - } - - /// - /// Returns the store value generation strategy to set for the given property. - /// - /// The property. - /// The store value generation strategy to set for the given property. - protected override ValueGenerated? GetValueGenerated(IConventionProperty property) - { - // TODO: move to relational? - if (property.DeclaringType.IsMappedToJson() -#pragma warning disable EF1001 // Internal EF Core API usage. - && property.IsOrdinalKeyProperty() -#pragma warning restore EF1001 // Internal EF Core API usage. - && (property.DeclaringType as IReadOnlyEntityType)?.FindOwnership()!.IsUnique == false) - { - return ValueGenerated.OnAdd; - } - - var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); - if (declaringTable.Name == null) - { - return null; - } - - // If the first mapping can be value generated then we'll consider all mappings to be value generated - // as this is a client-side configuration and can't be specified per-table. - return GetValueGenerated(property, declaringTable, Dependencies.TypeMappingSource); - } - - /// - /// Returns the store value generation strategy to set for the given property. - /// - /// The property. - /// The identifier of the store object. - /// The store value generation strategy to set for the given property. - public static new ValueGenerated? GetValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) - ?? (property.GetValueGenerationStrategy(storeObject) != NpgsqlValueGenerationStrategy.None - ? ValueGenerated.OnAdd - : null); - - private ValueGenerated? GetValueGenerated( - IReadOnlyProperty property, - in StoreObjectIdentifier storeObject, - ITypeMappingSource typeMappingSource) - => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) - ?? (property.GetValueGenerationStrategy(storeObject, typeMappingSource) != NpgsqlValueGenerationStrategy.None - ? ValueGenerated.OnAdd - : null); -} diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs deleted file mode 100644 index 5628daf138..0000000000 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationStrategyConvention.cs +++ /dev/null @@ -1,120 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -/// -/// A convention that configures the default model as -/// for newer PostgreSQL versions, -/// and for pre-10.0 versions. -/// -public class NpgsqlValueGenerationStrategyConvention : IModelInitializedConvention, IModelFinalizingConvention -{ - private readonly Version? _postgresVersion; - - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - /// The PostgreSQL version being targeted. This affects the default value generation strategy. - public NpgsqlValueGenerationStrategyConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies, - Version? postgresVersion) - { - Dependencies = dependencies; - RelationalDependencies = relationalDependencies; - _postgresVersion = postgresVersion; - } - - /// - /// Parameter object containing service dependencies. - /// - protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } - - /// - /// Relational provider-specific dependencies for this service. - /// - protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } - - /// - public virtual void ProcessModelInitialized(IConventionModelBuilder modelBuilder, IConventionContext context) - => modelBuilder.HasValueGenerationStrategy( - _postgresVersion < new Version(10, 0) - ? NpgsqlValueGenerationStrategy.SerialColumn - : NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - /// - public virtual void ProcessModelFinalizing( - IConventionModelBuilder modelBuilder, - IConventionContext context) - { - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) - { - foreach (var property in entityType.GetDeclaredProperties()) - { - NpgsqlValueGenerationStrategy? strategy = null; - var declaringTable = property.GetMappedStoreObjects(StoreObjectType.Table).FirstOrDefault(); - if (declaringTable.Name != null!) - { - strategy = property.GetValueGenerationStrategy(declaringTable, Dependencies.TypeMappingSource); - if (strategy == NpgsqlValueGenerationStrategy.None - && !IsStrategyNoneNeeded(property, declaringTable)) - { - strategy = null; - } - } - else - { - var declaringView = property.GetMappedStoreObjects(StoreObjectType.View).FirstOrDefault(); - if (declaringView.Name != null!) - { - strategy = property.GetValueGenerationStrategy(declaringView, Dependencies.TypeMappingSource); - if (strategy == NpgsqlValueGenerationStrategy.None - && !IsStrategyNoneNeeded(property, declaringView)) - { - strategy = null; - } - } - } - - // Needed for the annotation to show up in the model snapshot - if (strategy != null - && declaringTable.Name != null) - { - property.Builder.HasValueGenerationStrategy(strategy); - - if (strategy == NpgsqlValueGenerationStrategy.Sequence) - { - var sequence = modelBuilder.HasSequence( - property.GetSequenceName(declaringTable) - ?? entityType.GetRootType().ShortName() + modelBuilder.Metadata.GetSequenceNameSuffix(), - property.GetSequenceSchema(declaringTable) - ?? modelBuilder.Metadata.GetSequenceSchema()).Metadata; - - property.Builder.HasDefaultValueSql( - RelationalDependencies.UpdateSqlGenerator.GenerateObtainNextSequenceValueOperation( - sequence.Name, sequence.Schema)); - } - } - } - } - - bool IsStrategyNoneNeeded(IReadOnlyProperty property, StoreObjectIdentifier storeObject) - { - if (property.ValueGenerated == ValueGenerated.OnAdd - && !property.TryGetDefaultValue(storeObject, out _) - && property.GetDefaultValueSql(storeObject) is null - && property.GetComputedColumnSql(storeObject) is null - && property.DeclaringType.Model.GetValueGenerationStrategy() != NpgsqlValueGenerationStrategy.None) - { - var providerClrType = (property.GetValueConverter() - ?? (property.FindRelationalTypeMapping(storeObject) - ?? Dependencies.TypeMappingSource.FindMapping((IProperty)property))?.Converter) - ?.ProviderClrType.UnwrapNullableType(); - - return providerClrType is not null && (providerClrType.IsInteger()); - } - - return false; - } - } -} diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs deleted file mode 100644 index 0f2dd325ef..0000000000 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs +++ /dev/null @@ -1,274 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.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 NpgsqlAnnotationNames -{ - /// - /// 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 const string Prefix = "Npgsql:"; - - /// - /// 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 const string CompressionMethod = Prefix + "Compression:"; - - /// - /// 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 const string CreatedConcurrently = Prefix + "CreatedConcurrently"; - - /// - /// 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 const string DatabaseTemplate = Prefix + "DatabaseTemplate"; - - /// - /// 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 const string HiLoSequenceName = Prefix + "HiLoSequenceName"; - - /// - /// 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 const string HiLoSequenceSchema = Prefix + "HiLoSequenceSchema"; - - /// - /// 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 const string IdentityOptions = Prefix + "IdentitySequenceOptions"; - - /// - /// 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 const string IndexMethod = Prefix + "IndexMethod"; - - /// - /// 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 const string IndexOperators = Prefix + "IndexOperators"; - - /// - /// 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 const string IndexNullSortOrder = Prefix + "IndexNullSortOrder"; - - /// - /// 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 const string IndexInclude = Prefix + "IndexInclude"; - - /// - /// 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 const string NullsDistinct = Prefix + "NullsDistinct"; - - /// - /// 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 const string Tablespace = Prefix + "Tablespace"; - - /// - /// 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 const string TsVectorConfig = Prefix + "TsVectorConfig"; - - /// - /// 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 const string TsVectorProperties = Prefix + "TsVectorProperties"; - - /// - /// 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 const string UnloggedTable = Prefix + "UnloggedTable"; - - /// - /// 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 const string ValueGenerationStrategy = Prefix + "ValueGenerationStrategy"; - - /// - /// 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 const string SequenceNameSuffix = Prefix + "SequenceNameSuffix"; - - /// - /// 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 const string SequenceName = Prefix + "SequenceName"; - - /// - /// 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 const string SequenceSchema = Prefix + "SequenceSchema"; - - /// - /// 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 const string CollationDefinitionPrefix = Prefix + "CollationDefinition:"; - - /// - /// 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 const string EnumPrefix = Prefix + "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 const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; - - /// - /// 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 const string RangePrefix = Prefix + "Range:"; - - /// - /// 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 const string StorageParameterPrefix = Prefix + "StorageParameter:"; - - // Database model annotations - - /// - /// Identifies the type of the PostgreSQL type of this column (e.g. array, range, base). - /// - public const string PostgresTypeType = Prefix + "PostgresTypeType"; - - /// - /// If this column's data type is an array, contains the data type of its elements. - /// Otherwise null. - /// - public const string ElementDataType = Prefix + "ElementDataType"; - - /// - /// If the index contains an expression (rather than simple column references), the expression is contained here. - /// This is currently unsupported and will be ignored. - /// - public const string IndexExpression = Prefix + "IndexExpression"; - - /// - /// 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 EF Core's standard model bulk configuration API")] - public const string DefaultColumnCollation = Prefix + "DefaultColumnCollation"; - - /// - /// 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("Replaced by ValueGenerationStrategy.SerialColumn")] - public const string ValueGeneratedOnAdd = Prefix + "ValueGeneratedOnAdd"; - - /// - /// 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("Replaced by built-in EF Core support, use HasComment on entities or properties.")] - public const string Comment = Prefix + "Comment"; - - /// - /// 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("Replaced by RelationalAnnotationNames.Collation")] - public const string IndexCollation = Prefix + "IndexCollation"; - - /// - /// 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. - /// - // Replaced by IsDescending in EF Core 7.0 - public const string IndexSortOrder = Prefix + "IndexSortOrder"; -} diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs deleted file mode 100644 index a9493d6aeb..0000000000 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.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 NpgsqlAnnotationProvider : RelationalAnnotationProvider -{ - /// - /// 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 NpgsqlAnnotationProvider(RelationalAnnotationProviderDependencies dependencies) - : base(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 - /// 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 IEnumerable For(ITable table, bool designTime) - { - if (!designTime) - { - yield break; - } - - // Model validation ensures that these facets are the same on all mapped entity types - var entityType = (IEntityType)table.EntityTypeMappings.First().TypeBase; - - if (entityType.GetIsUnlogged()) - { - yield return new Annotation(NpgsqlAnnotationNames.UnloggedTable, entityType.GetIsUnlogged()); - } - - if (entityType[CockroachDbAnnotationNames.InterleaveInParent] is not null) - { - yield return new Annotation( - CockroachDbAnnotationNames.InterleaveInParent, entityType[CockroachDbAnnotationNames.InterleaveInParent]); - } - - foreach (var storageParamAnnotation in entityType.GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - yield return storageParamAnnotation; - } - } - - /// - /// 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 IEnumerable For(IColumn column, bool designTime) - { - if (!designTime) - { - yield break; - } - - var table = StoreObjectIdentifier.Table(column.Table.Name, column.Table.Schema); - var valueGeneratedProperty = column.PropertyMappings.Where( - m => (m.TableMapping.IsSharedTablePrincipal ?? true) - && m.TableMapping.TypeBase == m.Property.DeclaringType) - .Select(m => m.Property) - .FirstOrDefault( - p => p.GetValueGenerationStrategy(table) switch - { - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn => true, - NpgsqlValueGenerationStrategy.IdentityAlwaysColumn => true, - NpgsqlValueGenerationStrategy.SerialColumn => true, - _ => false - }); - - if (valueGeneratedProperty is not null) - { - var valueGenerationStrategy = valueGeneratedProperty.GetValueGenerationStrategy(); - yield return new Annotation(NpgsqlAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy); - - if (valueGenerationStrategy is NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn) - { - if (valueGeneratedProperty[NpgsqlAnnotationNames.IdentityOptions] is string identityOptions) - { - yield return new Annotation(NpgsqlAnnotationNames.IdentityOptions, identityOptions); - } - } - } - - if (column.PropertyMappings.Select(m => m.Property.GetTsVectorConfig()) - .FirstOrDefault(c => c is not null) is { } tsVectorConfig) - { - yield return new Annotation(NpgsqlAnnotationNames.TsVectorConfig, tsVectorConfig); - } - - valueGeneratedProperty = column.PropertyMappings.Select(m => m.Property) - .FirstOrDefault(p => p.GetTsVectorProperties() is not null); - if (valueGeneratedProperty is not null) - { - var tableIdentifier = StoreObjectIdentifier.Table(column.Table.Name, column.Table.Schema); - - yield return new Annotation( - NpgsqlAnnotationNames.TsVectorProperties, - valueGeneratedProperty.GetTsVectorProperties()! - .Select(p2 => valueGeneratedProperty.DeclaringType.FindProperty(p2)!.GetColumnName(tableIdentifier)) - .ToArray()); - } - - // JSON columns have no property mappings so all annotations that rely on property mappings should be skipped for them - if (column is not JsonColumn - && column.PropertyMappings.FirstOrDefault()?.Property.GetCompressionMethod() is { } compressionMethod) - { - // Model validation ensures that these facets are the same on all mapped properties - yield return new Annotation(NpgsqlAnnotationNames.CompressionMethod, compressionMethod); - } - } - - /// - /// 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 IEnumerable For(ITableIndex index, bool designTime) - { - if (!designTime) - { - yield break; - } - - // Model validation ensures that these facets are the same on all mapped indexes - var modelIndex = index.MappedIndexes.First(); - - if (modelIndex.GetCollation() is { } collation) - { - yield return new Annotation(RelationalAnnotationNames.Collation, collation); - } - - if (modelIndex.GetMethod() is { } method) - { - yield return new Annotation(NpgsqlAnnotationNames.IndexMethod, method); - } - - if (modelIndex.GetOperators() is { } operators) - { - yield return new Annotation(NpgsqlAnnotationNames.IndexOperators, operators); - } - - if (modelIndex.GetNullSortOrder() is { } nullSortOrder) - { - yield return new Annotation(NpgsqlAnnotationNames.IndexNullSortOrder, nullSortOrder); - } - - if (modelIndex.GetTsVectorConfig() is { } configName) - { - yield return new Annotation(NpgsqlAnnotationNames.TsVectorConfig, configName); - } - - if (modelIndex.GetIncludeProperties() is { } includeProperties) - { - var tableIdentifier = StoreObjectIdentifier.Table(index.Table.Name, index.Table.Schema); - - yield return new Annotation( - NpgsqlAnnotationNames.IndexInclude, - includeProperties - .Select(p => modelIndex.DeclaringEntityType.FindProperty(p)!.GetColumnName(tableIdentifier)) - .ToArray()); - } - - if (modelIndex.IsCreatedConcurrently() is { } isCreatedConcurrently) - { - yield return new Annotation(NpgsqlAnnotationNames.CreatedConcurrently, isCreatedConcurrently); - } - - if (modelIndex.GetAreNullsDistinct() is { } nullsDistinct) - { - yield return new Annotation(NpgsqlAnnotationNames.NullsDistinct, nullsDistinct); - } - - foreach (var storageParamAnnotation in modelIndex.GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal))) - { - yield return storageParamAnnotation; - } - - // Support legacy annotation for index ordering - if (modelIndex[NpgsqlAnnotationNames.IndexSortOrder] is IReadOnlyList legacySortOrder) - { - yield return new Annotation(NpgsqlAnnotationNames.IndexSortOrder, legacySortOrder); - } - } - - /// - /// 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 IEnumerable For(IRelationalModel model, bool designTime) - { - if (!designTime) - { - return []; - } - - return model.Model.GetAnnotations().Where( - a => - a.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)); - } -} diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlIndexExtensions.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlIndexExtensions.cs deleted file mode 100644 index 24028b779e..0000000000 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlIndexExtensions.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore.Metadata.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 NpgsqlIndexExtensions -{ - /// - /// 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 AreCompatibleForNpgsql( - this IReadOnlyIndex index, - IReadOnlyIndex duplicateIndex, - in StoreObjectIdentifier storeObject, - bool shouldThrow) - { - if (index.GetIncludeProperties() != duplicateIndex.GetIncludeProperties()) - { - if (index.GetIncludeProperties() is null - || duplicateIndex.GetIncludeProperties() is null - || !SameColumnNames(index, duplicateIndex, storeObject)) - { - if (shouldThrow) - { - throw new InvalidOperationException( - NpgsqlStrings.DuplicateIndexIncludedMismatch( - index.Properties.Format(), - index.DeclaringEntityType.DisplayName(), - duplicateIndex.Properties.Format(), - duplicateIndex.DeclaringEntityType.DisplayName(), - index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(storeObject), - FormatInclude(index, storeObject), - FormatInclude(duplicateIndex, storeObject))); - } - - return false; - } - } - - if (index.IsCreatedConcurrently() != duplicateIndex.IsCreatedConcurrently()) - { - if (shouldThrow) - { - throw new InvalidOperationException( - NpgsqlStrings.DuplicateIndexConcurrentCreationMismatch( - index.Properties.Format(), - index.DeclaringEntityType.DisplayName(), - duplicateIndex.Properties.Format(), - duplicateIndex.DeclaringEntityType.DisplayName(), - index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(storeObject))); - } - - return false; - } - - if (index.GetCollation() != duplicateIndex.GetCollation()) - { - if (shouldThrow) - { - throw new InvalidOperationException( - NpgsqlStrings.DuplicateIndexCollationMismatch( - index.Properties.Format(), - index.DeclaringEntityType.DisplayName(), - duplicateIndex.Properties.Format(), - duplicateIndex.DeclaringEntityType.DisplayName(), - index.DeclaringEntityType.GetSchemaQualifiedTableName(), - index.GetDatabaseName(storeObject))); - } - - return false; - } - - return true; - - static bool SameColumnNames(IReadOnlyIndex index, IReadOnlyIndex duplicateIndex, StoreObjectIdentifier storeObject) - => index.GetIncludeProperties()!.Select( - p => index.DeclaringEntityType.FindProperty(p)!.GetColumnName(storeObject)) - .SequenceEqual( - duplicateIndex.GetIncludeProperties()!.Select( - p => duplicateIndex.DeclaringEntityType.FindProperty(p)!.GetColumnName(storeObject))); - } - - private static string FormatInclude(IReadOnlyIndex index, StoreObjectIdentifier storeObject) - => index.GetIncludeProperties() is null - ? "{}" - : "{'" - + string.Join( - "', '", - index.GetIncludeProperties()!.Select( - p => index.DeclaringEntityType.FindProperty(p) - ?.GetColumnName(storeObject))) - + "'}"; -} diff --git a/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs b/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs deleted file mode 100644 index 3e83179bf3..0000000000 --- a/src/EFCore.PG/Metadata/NpgsqlValueGenerationStrategy.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -/// -/// Defines strategies to use when generating values for database columns. -/// -/// -/// See Model building conventions. -/// -public enum NpgsqlValueGenerationStrategy -{ - /// - /// No Npgsql-specific strategy. - /// - None, - - /// - /// - /// A sequence-based hi-lo pattern where blocks of IDs are allocated from the server and - /// used client-side for generating keys. - /// - /// - /// This is an advanced pattern--only use this strategy if you are certain it is what you need. - /// - /// - SequenceHiLo, - - /// - /// - /// Selects the serial column strategy, which is a regular column backed by an auto-created index. - /// - /// - /// If you are creating a new project on PostgreSQL 10 or above, consider using instead. - /// - /// - SerialColumn, - - /// - /// Selects the always-identity column strategy (a value cannot be provided). - /// Available only starting PostgreSQL 10. - /// - IdentityAlwaysColumn, - - /// - /// Selects the by-default-identity column strategy (a value can be provided to override the identity mechanism). - /// Available only starting PostgreSQL 10. - /// - IdentityByDefaultColumn, - - /// - /// A pattern that uses a database sequence to generate values for the column. - /// - Sequence -} - -/// -/// Extension methods over . -/// -public static class NpgsqlValueGenerationStrategyExtensions -{ - /// - /// Whether the given strategy is either or - /// . - /// - public static bool IsIdentity(this NpgsqlValueGenerationStrategy strategy) - => strategy is NpgsqlValueGenerationStrategy.IdentityByDefaultColumn or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn; - - /// - /// Whether the given strategy is either or - /// . - /// - public static bool IsIdentity(this NpgsqlValueGenerationStrategy? strategy) - => strategy is NpgsqlValueGenerationStrategy.IdentityByDefaultColumn or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn; -} diff --git a/src/EFCore.PG/Metadata/PostgresCollation.cs b/src/EFCore.PG/Metadata/PostgresCollation.cs deleted file mode 100644 index a259a6a680..0000000000 --- a/src/EFCore.PG/Metadata/PostgresCollation.cs +++ /dev/null @@ -1,215 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -/// -/// 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 PostgresCollation -{ - private readonly IReadOnlyAnnotatable _annotatable; - private readonly string _annotationName; - - internal PostgresCollation(IReadOnlyAnnotatable annotatable, string annotationName) - { - _annotatable = Check.NotNull(annotatable, nameof(annotatable)); - _annotationName = Check.NotNull(annotationName, nameof(annotationName)); - } - - /// - /// 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 PostgresCollation GetOrAddCollation( - IMutableAnnotatable annotatable, - string? schema, - string name, - string lcCollate, - string lcCtype, - string? provider = null, - bool? deterministic = null) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - if (FindCollation(annotatable, schema, name) is { } collation) - { - return collation; - } - - var annotationName = BuildAnnotationName(schema, name); - - return new PostgresCollation(annotatable, annotationName) - { - LcCollate = lcCollate, - LcCtype = lcCtype, - Provider = provider, - IsDeterministic = deterministic - }; - } - - /// - /// 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 PostgresCollation? FindCollation( - IReadOnlyAnnotatable annotatable, - string? schema, - string name) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - var annotationName = BuildAnnotationName(schema, name); - - return annotatable[annotationName] is null ? null : new PostgresCollation(annotatable, annotationName); - } - - private static string BuildAnnotationName(string? schema, string name) - => schema is not null - ? $"{NpgsqlAnnotationNames.CollationDefinitionPrefix}{schema}.{name}" - : $"{NpgsqlAnnotationNames.CollationDefinitionPrefix}{name}"; - - /// - /// 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 IEnumerable GetCollations(IReadOnlyAnnotatable annotatable) - => Check.NotNull(annotatable, nameof(annotatable)) - .GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)) - .Select(a => new PostgresCollation(annotatable, a.Name)); - - /// - /// 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 Annotatable Annotatable - => (Annotatable)_annotatable; - - /// - /// 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 string? Schema - => GetData().Schema; - - /// - /// 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 string Name - => GetData().Name!; - - /// - /// 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 string LcCollate - { - get => GetData().LcCollate!; - set => SetData(lcCollate: value); - } - - /// - /// 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 string LcCtype - { - get => GetData().LcCtype!; - set => SetData(lcCtype: value); - } - - /// - /// 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 string? Provider - { - get => GetData().Provider; - set => SetData(provider: value); - } - - /// - /// 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 bool? IsDeterministic - { - get => GetData().IsDeterministic; - set => SetData(deterministic: value); - } - - private (string? Schema, string? Name, string? LcCollate, string? LcCtype, string? Provider, bool? IsDeterministic) GetData() - => Deserialize(Annotatable.FindAnnotation(_annotationName)); - - private void SetData(string? lcCollate = null, string? lcCtype = null, string? provider = null, bool? deterministic = null) - => Annotatable[_annotationName] = - $"{lcCollate ?? LcCollate},{lcCtype ?? LcCtype},{provider ?? Provider},{deterministic ?? IsDeterministic}"; - - private static (string? Schema, string? Name, string? LcCollate, string? LcCtype, string? Provider, bool? IsDeterministic) - Deserialize(IAnnotation? annotation) - { - if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) - { - return (null, null!, null!, null!, null, null); - } - - string?[] elements = value.Split(','); - if (elements.Length != 4) - { - throw new ArgumentException($"Cannot parse collation annotation value: {value}"); - } - - for (var i = 0; i < 4; i++) - { - if (elements[i] == "") - { - elements[i] = null; - } - } - - var isDeterministic = elements[3] is { } isDeterminsticString - ? bool.Parse(isDeterminsticString) - : (bool?)null; - - // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs). - // Yes, this doesn't support dots in the schema/collation name, let somebody complain first. - var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.CollationDefinitionPrefix.Length).Split('.'); - switch (schemaAndName.Length) - { - case 1: - return (null, schemaAndName[0], elements[0], elements[1], elements[2], isDeterministic); - case 2: - return (schemaAndName[0], schemaAndName[1], elements[0], elements[1], elements[2], isDeterministic); - default: - throw new ArgumentException($"Cannot parse collation name from annotation: {annotation.Name}"); - } - } -} diff --git a/src/EFCore.PG/Metadata/PostgresEnum.cs b/src/EFCore.PG/Metadata/PostgresEnum.cs deleted file mode 100644 index 0d373c0cc3..0000000000 --- a/src/EFCore.PG/Metadata/PostgresEnum.cs +++ /dev/null @@ -1,248 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -/// -/// Represents the metadata for a PostgreSQL enum. -/// -public class PostgresEnum -{ - private readonly IReadOnlyAnnotatable _annotatable; - private readonly string _annotationName; - - /// - /// Creates a . - /// - /// The annotatable to search for the annotation. - /// The annotation name to search for in the annotatable. - /// - /// - /// - /// - /// - /// - internal PostgresEnum(IReadOnlyAnnotatable annotatable, string annotationName) - { - _annotatable = Check.NotNull(annotatable, nameof(annotatable)); - _annotationName = Check.NotNull(annotationName, nameof(annotationName)); - } - - /// - /// 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( - IMutableAnnotatable 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 . - /// - /// 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 . - /// - /// The annotatable from which to get or add the enum. - /// The enum name. - /// The enum labels. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresEnum GetOrAddPostgresEnum( - IMutableAnnotatable annotatable, - string name, - string[] labels) - => GetOrAddPostgresEnum(annotatable, null, name, labels); - - /// - /// Finds a in the , or returns null if not found. - /// - /// The annotatable to search for the enum. - /// The enum schema or null to use the model's default schema. - /// The enum name. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresEnum? FindPostgresEnum( - IReadOnlyAnnotatable annotatable, - string? schema, - string name) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - var annotationName = BuildAnnotationName(schema, name); - - return annotatable[annotationName] is null ? null : new PostgresEnum(annotatable, annotationName); - } - - private static string BuildAnnotationName(string? schema, string name) - => schema is not null - ? $"{NpgsqlAnnotationNames.EnumPrefix}{schema}.{name}" - : $"{NpgsqlAnnotationNames.EnumPrefix}{name}"; - - /// - /// Gets the collection of stored in the . - /// - /// The annotatable to search for annotations. - /// - /// The collection of stored in the . - /// - /// - /// - /// - public static IEnumerable GetPostgresEnums(IReadOnlyAnnotatable annotatable) - => Check.NotNull(annotatable, nameof(annotatable)) - .GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal)) - .Select(a => new PostgresEnum(annotatable, a.Name)); - - /// - /// The that stores the enum. - /// - public virtual Annotatable Annotatable - => (Annotatable)_annotatable; - - /// - /// The enum schema or null to represent the default schema. - /// - public virtual string? Schema - => GetData().Schema; - - /// - /// The enum name. - /// - public virtual string Name - => GetData().Name!; - - /// - /// The enum labels. - /// - public virtual IReadOnlyList Labels - { - get => GetData().Labels!; - set => SetData(value); - } - - private (string? Schema, string? Name, string[]? Labels) GetData() - => Deserialize(Annotatable.FindAnnotation(_annotationName)); - - private void SetData(IEnumerable labels) - => Annotatable[_annotationName] = string.Join(",", labels); - - private static (string? Schema, string? Name, string[]? Labels) Deserialize(IAnnotation? annotation) - { - if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) - { - return (null, null, null); - } - - var labels = value.Split(','); - - // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs). - // Yes, this doesn't support dots in the schema/enum name, let somebody complain first. - var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.EnumPrefix.Length).Split('.'); - switch (schemaAndName.Length) - { - case 1: - return (null, schemaAndName[0], labels); - case 2: - return (schemaAndName[0], schemaAndName[1], labels); - default: - throw new ArgumentException($"Cannot parse enum name from annotation: {annotation.Name}"); - } - } -} diff --git a/src/EFCore.PG/Metadata/PostgresExtension.cs b/src/EFCore.PG/Metadata/PostgresExtension.cs deleted file mode 100644 index bfd42c28c6..0000000000 --- a/src/EFCore.PG/Metadata/PostgresExtension.cs +++ /dev/null @@ -1,238 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -/// -/// Represents the metadata for a PostgreSQL extension. -/// -public class PostgresExtension -{ - private readonly IReadOnlyAnnotatable _annotatable; - private readonly string _annotationName; - - /// - /// Creates a . - /// - /// The annotatable to search for the annotation. - /// The annotation name to search for in the annotatable. - /// - /// - /// - /// - /// - /// - internal PostgresExtension(IReadOnlyAnnotatable annotatable, string annotationName) - { - _annotatable = Check.NotNull(annotatable, nameof(annotatable)); - _annotationName = Check.NotNull(annotationName, nameof(annotationName)); - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the extension. - /// The extension schema or null to use the model's default schema. - /// The extension name. - /// The extension version. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresExtension GetOrAddPostgresExtension( - IMutableAnnotatable annotatable, - string? schema, - string name, - string? version) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotNull(name, nameof(name)); - - if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) - { - return postgresExtension; - } - - var annotationName = BuildAnnotationName(schema, name); - - return new PostgresExtension(annotatable, annotationName) { Version = version }; - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the extension. - /// The extension schema or null to use the model's default schema. - /// The extension name. - /// The extension version. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresExtension GetOrAddPostgresExtension( - IConventionAnnotatable annotatable, - string? schema, - string name, - string? version) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotNull(name, nameof(name)); - - if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) - { - return postgresExtension; - } - - var annotationName = BuildAnnotationName(schema, name); - - return new PostgresExtension(annotatable, annotationName) { Version = version }; - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the extension. - /// The extension name. - /// The extension version. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - public static PostgresExtension GetOrAddPostgresExtension( - IMutableAnnotatable annotatable, - string name, - string? version) - => GetOrAddPostgresExtension(annotatable, null, name, version); - - /// - /// Finds a in the , or returns null if not found. - /// - /// The annotatable to search for the extension. - /// The extension schema. The default schema is never used. - /// The extension name. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresExtension? FindPostgresExtension( - IReadOnlyAnnotatable annotatable, - string? schema, - string name) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - var annotationName = BuildAnnotationName(schema, name); - - return annotatable[annotationName] is null ? null : new PostgresExtension(annotatable, annotationName); - } - - internal static string BuildAnnotationName(string? schema, string name) - => schema is not null - ? $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}{schema}.{name}" - : $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}{name}"; - - /// - /// Gets the collection of stored in the . - /// - /// The annotatable to search for annotations. - /// - /// The collection of stored in the . - /// - /// - /// - /// - public static IEnumerable GetPostgresExtensions(IReadOnlyAnnotatable annotatable) - => Check.NotNull(annotatable, nameof(annotatable)) - .GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal)) - .Select(a => new PostgresExtension(annotatable, a.Name)); - - /// - /// The that stores the extension. - /// - public virtual Annotatable Annotatable - => (Annotatable)_annotatable; - - /// - /// The extension schema or null to represent the default schema. - /// - public virtual string? Schema - => GetData().Schema; - - /// - /// The extension name. - /// - public virtual string Name - => GetData().Name!; - - /// - /// The extension version. - /// - public virtual string? Version - { - get => GetData().Version; - set => SetData(value); - } - - private (string? Schema, string? Name, string? Version) GetData() - => Deserialize(Annotatable.FindAnnotation(_annotationName)!); - - private void SetData(string? version) - { - var data = GetData(); - Annotatable[_annotationName] = $"{data.Schema},{data.Name},{version}"; - } - - private static (string? Schema, string? Name, string? Version) Deserialize(IAnnotation? annotation) - { - if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) - { - return (null, null, null); - } - - // TODO: Can't actually use schema and name...they might not be set when this is first called. - var schemaNameValue = value.Split(',').Select(x => x.Trim()).Select(x => x is "" or "''" ? null : x).ToArray(); - var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.PostgresExtensionPrefix.Length).Split('.'); - switch (schemaAndName.Length) - { - case 1: - return (null, schemaAndName[0], schemaNameValue[2]); - case 2: - return (schemaAndName[0], schemaAndName[1], schemaNameValue[2]); - default: - throw new ArgumentException($"Cannot parse extension name from annotation: {annotation.Name}"); - } - } -} diff --git a/src/EFCore.PG/Metadata/PostgresRange.cs b/src/EFCore.PG/Metadata/PostgresRange.cs deleted file mode 100644 index 6738519673..0000000000 --- a/src/EFCore.PG/Metadata/PostgresRange.cs +++ /dev/null @@ -1,253 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -/// -/// Represents the metadata for a PostgreSQL range. -/// -public class PostgresRange -{ - private readonly IReadOnlyAnnotatable _annotatable; - private readonly string _annotationName; - - /// - /// Creates a . - /// - /// The annotatable to search for the annotation. - /// The annotation name to search for in the annotatable. - /// - /// - /// - /// - /// - /// - internal PostgresRange(IReadOnlyAnnotatable annotatable, string annotationName) - { - _annotatable = Check.NotNull(annotatable, nameof(annotatable)); - _annotationName = Check.NotNull(annotationName, nameof(annotationName)); - } - - /// - /// Gets or adds a from or to the . - /// - /// The annotatable from which to get or add the range. - /// The range schema or null to use the model's default schema. - /// The range name. - /// The range subtype. - /// - /// - /// - /// - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresRange GetOrAddPostgresRange( - IMutableAnnotatable annotatable, - string? schema, - string name, - string subtype, - string? canonicalFunction = null, - string? subtypeOpClass = null, - string? collation = null, - string? subtypeDiff = null) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - Check.NotNull(subtype, nameof(subtype)); - - if (FindPostgresRange(annotatable, schema, name) is { } postgresRange) - { - return postgresRange; - } - - var annotationName = BuildAnnotationName(schema, name); - - return new PostgresRange(annotatable, annotationName) - { - Subtype = subtype, - CanonicalFunction = canonicalFunction, - SubtypeOpClass = subtypeOpClass, - Collation = collation, - SubtypeDiff = subtypeDiff, - }; - } - - /// - /// Finds a in the , or returns null if not found. - /// - /// The annotatable to search for the range. - /// The range schema or null to use the model's default schema. - /// The range name. - /// - /// The from the . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static PostgresRange? FindPostgresRange( - IReadOnlyAnnotatable annotatable, - string? schema, - string name) - { - Check.NotNull(annotatable, nameof(annotatable)); - Check.NullButNotEmpty(schema, nameof(schema)); - Check.NotEmpty(name, nameof(name)); - - var annotationName = BuildAnnotationName(schema, name); - - return annotatable[annotationName] is null ? null : new PostgresRange(annotatable, annotationName); - } - - private static string BuildAnnotationName(string? schema, string name) - => schema is not null - ? $"{NpgsqlAnnotationNames.RangePrefix}{schema}.{name}" - : $"{NpgsqlAnnotationNames.RangePrefix}{name}"; - - /// - /// Gets the collection of stored in the . - /// - /// The annotatable to search for annotations. - /// - /// The collection of stored in the . - /// - /// - /// - /// - public static IEnumerable GetPostgresRanges(IReadOnlyAnnotatable annotatable) - => Check.NotNull(annotatable, nameof(annotatable)) - .GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal)) - .Select(a => new PostgresRange(annotatable, a.Name)); - - /// - /// The that stores the range. - /// - public virtual Annotatable Annotatable - => (Annotatable)_annotatable; - - /// - /// The range schema or null to represent the default schema. - /// - public virtual string? Schema - => GetData().Schema; - - /// - /// The range name. - /// - public virtual string Name - => GetData().Name!; - - /// - /// The subtype of the range. - /// - public virtual string Subtype - { - get => GetData().Subtype!; - set => SetData(subtype: value); - } - - /// - /// The function defining a "step" in a discrete range. - /// - public virtual string? CanonicalFunction - { - get => GetData().CanonicalFunction; - set => SetData(canonicalFunction: value); - } - - /// - /// The operator class to use. - /// - public virtual string? SubtypeOpClass - { - get => GetData().SubtypeOpClass; - set => SetData(subtypeOpClass: value); - } - - /// - /// The collation to use. - /// - public virtual string? Collation - { - get => GetData().Collation; - set => SetData(collation: value); - } - - /// - /// The function defining a difference in subtype values. - /// - public virtual string? SubtypeDiff - { - get => GetData().SubtypeDiff; - set => SetData(subtypeDiff: value); - } - - private (string? Schema, string? Name, string? Subtype, string? CanonicalFunction, string? SubtypeOpClass, string? Collation, string? - SubtypeDiff) GetData() - => Deserialize(Annotatable.FindAnnotation(_annotationName)!); - - private void SetData( - string? subtype = null, - string? canonicalFunction = null, - string? subtypeOpClass = null, - string? collation = null, - string? subtypeDiff = null) - => Annotatable[_annotationName] = - $"{subtype ?? Subtype},{canonicalFunction ?? CanonicalFunction},{subtypeOpClass ?? SubtypeOpClass},{collation ?? Collation},{subtypeDiff ?? SubtypeDiff}"; - - private static (string? Schema, string? Name, string? Subtype, string? CanonicalFunction, string? SubtypeOpClass, string? Collation, - string? SubtypeDiff) - Deserialize(IAnnotation? annotation) - { - if (annotation is null || !(annotation.Value is string value) || string.IsNullOrEmpty(value)) - { - return (null, null, null, null, null, null, null); - } - - string?[] elements = value.Split(','); - if (elements.Length != 5) - { - throw new ArgumentException($"Cannot parse range annotation value: {value}"); - } - - for (var i = 0; i < 5; i++) - { - if (elements[i] == "") - { - elements[i] = null; - } - } - - // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs). - // Yes, this doesn't support dots in the schema/range name, let somebody complain first. - var schemaAndName = annotation.Name.Substring(NpgsqlAnnotationNames.RangePrefix.Length).Split('.'); - switch (schemaAndName.Length) - { - case 1: - return (null, schemaAndName[0], elements[0], elements[1], elements[2], elements[3], elements[4]); - case 2: - return (schemaAndName[0], schemaAndName[1], elements[0], elements[1], elements[2], elements[3], elements[4]); - default: - throw new ArgumentException($"Cannot parse range name from annotation: {annotation.Name}"); - } - } -} diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs deleted file mode 100644 index c2834eed5f..0000000000 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlHistoryRepository.cs +++ /dev/null @@ -1,303 +0,0 @@ -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 -/// 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 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 - /// 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 NpgsqlHistoryRepository(HistoryRepositoryDependencies dependencies) - : base(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 - /// 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 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() - { - 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 - /// 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 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() - }]); - - /// - /// 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 string GetCreateIfNotExistsScript() - { - var script = GetCreateScript(); - return script.Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); - } - - /// - /// 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 string GetBeginIfNotExistsScript(string migrationId) - => $""" - -DO $EF$ -BEGIN - 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 - /// 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 string GetBeginIfExistsScript(string migrationId) - => $""" -DO $EF$ -BEGIN - 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 - /// 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 string GetEndIfScript() - => """ - 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 deleted file mode 100644 index 601af56041..0000000000 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrator.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; - -// Migrator is EF-pubternal, but overriding it is the only way to force Npgsql to ReloadTypes() after executing a migration which adds -// types to the database -#pragma warning disable EF1001 - -/// -/// 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 NpgsqlMigrator : Migrator -{ - private readonly IHistoryRepository _historyRepository; - private readonly IRelationalConnection _connection; - - /// - /// 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 NpgsqlMigrator( - IMigrationsAssembly migrationsAssembly, - IHistoryRepository historyRepository, - IDatabaseCreator databaseCreator, - IMigrationsSqlGenerator migrationsSqlGenerator, - IRawSqlCommandBuilder rawSqlCommandBuilder, - IMigrationCommandExecutor migrationCommandExecutor, - IRelationalConnection connection, - ISqlGenerationHelper sqlGenerationHelper, - ICurrentDbContext currentContext, - IModelRuntimeInitializer modelRuntimeInitializer, - IDiagnosticsLogger logger, - IRelationalCommandDiagnosticsLogger commandLogger, - IDatabaseProvider databaseProvider, - IMigrationsModelDiffer migrationsModelDiffer, - IDesignTimeModel designTimeModel, - IDbContextOptions contextOptions, - IExecutionStrategy executionStrategy) - : base(migrationsAssembly, historyRepository, databaseCreator, migrationsSqlGenerator, rawSqlCommandBuilder, - migrationCommandExecutor, connection, sqlGenerationHelper, currentContext, modelRuntimeInitializer, logger, - commandLogger, databaseProvider, migrationsModelDiffer, designTimeModel, contextOptions, executionStrategy) - { - _historyRepository = historyRepository; - _connection = connection; - } - - /// - /// 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 Migrate(string? targetMigration) - { - var appliedMigrations = _historyRepository.GetAppliedMigrations(); - - base.Migrate(targetMigration); - - PopulateMigrations( - appliedMigrations.Select(t => t.MigrationId), - targetMigration, - out var migratorData); - - 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 = migratorData.AppliedMigrations.Count > 0 ? migratorData.AppliedMigrations : migratorData.RevertedMigrations; - var reloadTypes = migrations - .SelectMany(m => m.UpOperations) - .OfType() - .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); - - if (reloadTypes && _connection.DbConnection is NpgsqlConnection npgsqlConnection) - { - _connection.Open(); - try - { - npgsqlConnection.ReloadTypes(); - } - finally - { - _connection.Close(); - } - } - } - - /// - /// 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 MigrateAsync(string? targetMigration, CancellationToken cancellationToken = default) - { - var appliedMigrations = await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false); - - await base.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false); - - PopulateMigrations( - appliedMigrations.Select(t => t.MigrationId), - targetMigration, - out var migratorData); - - 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 = migratorData.AppliedMigrations.Count > 0 ? migratorData.AppliedMigrations : migratorData.RevertedMigrations; - var reloadTypes = migrations - .SelectMany(m => m.UpOperations) - .OfType() - .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); - - if (reloadTypes && _connection.DbConnection is NpgsqlConnection npgsqlConnection) - { - await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); - try - { - await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); - } - finally - { - await _connection.CloseAsync().ConfigureAwait(false); - } - } - } -} diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs deleted file mode 100644 index 6c74ef60a7..0000000000 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ /dev/null @@ -1,2315 +0,0 @@ -using System.Globalization; -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; - -/// -/// PostgreSQL-specific implementation of . -/// -/// -/// -/// The service lifetime is . This means that each instance will use -/// its own instance of this service. The implementation may depend on other services registered with any lifetime. The -/// implementation does not need to be thread-safe. -/// -/// -/// See Database migrations. -/// -/// -public class NpgsqlMigrationsSqlGenerator : MigrationsSqlGenerator -{ - private IReadOnlyList _operations = null!; - private readonly RelationalTypeMapping _stringTypeMapping; - - /// - /// The backend version to target. - /// - private readonly Version _postgresVersion; - - /// - /// Creates a new instance. - /// - /// Parameter object containing dependencies for this service. - /// The singleton options to use. - public NpgsqlMigrationsSqlGenerator( - MigrationsSqlGeneratorDependencies dependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies) - { - _postgresVersion = npgsqlSingletonOptions.PostgresVersion; - _stringTypeMapping = dependencies.TypeMappingSource.GetMapping(typeof(string)) - ?? throw new InvalidOperationException("No string type mapping found"); - } - - /// - public override IReadOnlyList Generate( - IReadOnlyList operations, - IModel? model = null, - MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) - { - IReadOnlyList results; - _operations = operations; - - try - { - results = base.Generate(operations, model, options); - - AddSequenceBumpingForSeeding(); - - return results; - } - finally - { - _operations = null!; - } - - void AddSequenceBumpingForSeeding() - { - // For all tables where we had data seeding insertions, find all columns mapped to properties with identity/serial value - // generation strategy. We'll bump the sequences for those columns. - var seededGeneratedColumns = operations - .OfType() - .Select(o => new { o.Schema, o.Table }) - .Distinct() - .SelectMany( - t => model?.GetRelationalModel().FindTable(t.Table, t.Schema)?.Columns - .Where( - c => c.PropertyMappings.Any( - p => p.Property.GetValueGenerationStrategy() is - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - or NpgsqlValueGenerationStrategy.IdentityAlwaysColumn - or NpgsqlValueGenerationStrategy.SerialColumn)) - ?? []) - .Distinct() - .ToArray(); - - if (!seededGeneratedColumns.Any()) - { - return; - } - - var builder = new MigrationCommandListBuilder(Dependencies); - - foreach (var c in seededGeneratedColumns) - { - // Weirdly, pg_get_serial_sequence accepts a standard quoted "schema"."table" inside its first - // parameter string literal, but the second one is a column name that shouldn't be double-quoted... - - var table = Dependencies.SqlGenerationHelper.DelimitIdentifier(c.Table.Name, c.Table.Schema); - var column = Dependencies.SqlGenerationHelper.DelimitIdentifier(c.Name); - var unquotedColumn = c.Name.Replace("'", "''"); - - // When generating idempotent scripts, migration DDL is enclosed in anonymous DO blocks, - // where PERFORM must be used instead of SELECT - var selectOrPerform = options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) - ? "PERFORM" - : "SELECT"; - - // Set the sequence's value to the greater of: - // 1. Maximum value currently present in the column (i.e. just seeded) - // 2. Current value of the sequence (the max value above could be out of range of the sequence, - // e.g. negative values seeded) - builder - .AppendLine( - $""" -{selectOrPerform} setval( - pg_get_serial_sequence('{table}', '{unquotedColumn}'), - GREATEST( - (SELECT MAX({column}) FROM {table}) + 1, - nextval(pg_get_serial_sequence('{table}', '{unquotedColumn}'))), - false); -"""); - } - - builder.EndCommand(); - - results = results.Concat(builder.GetCommandList()).ToArray(); - } - } - - /// - protected override void Generate(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - if (operation is NpgsqlCreateDatabaseOperation createDatabaseOperation) - { - Generate(createDatabaseOperation, model, builder); - return; - } - - if (operation is NpgsqlDropDatabaseOperation dropDatabaseOperation) - { - Generate(dropDatabaseOperation, model, builder); - return; - } - - base.Generate(operation, model, builder); - } - - #region Standard migrations - - /// - protected override void Generate( - CreateTableOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - if (!terminate && operation.Comment is not null) - { - throw new ArgumentException( - $"When generating migrations SQL for {nameof(CreateTableOperation)}, can't produce unterminated SQL with comments"); - } - - operation.Columns.RemoveAll(c => IsSystemColumn(c.Name)); - - builder.Append("CREATE "); - - if (operation[NpgsqlAnnotationNames.UnloggedTable] is true) - { - builder.Append("UNLOGGED "); - } - - builder - .Append("TABLE ") - .Append(DelimitIdentifier(operation.Name, operation.Schema)) - .AppendLine(" ("); - - using (builder.Indent()) - { - CreateTableColumns(operation, model, builder); - CreateTableConstraints(operation, model, builder); - builder.AppendLine(); - } - - builder.Append(")"); - - // CockroachDB "interleave in parent" (https://www.cockroachlabs.com/docs/stable/interleave-in-parent.html) - if (operation[CockroachDbAnnotationNames.InterleaveInParent] is string) - { - var interleaveInParent = new CockroachDbInterleaveInParent(operation); - var parentTableSchema = interleaveInParent.ParentTableSchema; - var parentTableName = interleaveInParent.ParentTableName; - var interleavePrefix = interleaveInParent.InterleavePrefix; - - builder - .AppendLine() - .Append("INTERLEAVE IN PARENT ") - .Append(DelimitIdentifier(parentTableName, parentTableSchema)) - .Append(" (") - .Append(string.Join(", ", interleavePrefix.Select(c => DelimitIdentifier(c)))) - .Append(")"); - } - - AppendStoreParameters(operation, builder, withLeadingNewline: true); - - // Comment on the table - if (operation.Comment is not null) - { - builder.AppendLine(";"); - - builder - .Append("COMMENT ON TABLE ") - .Append(DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" IS ") - .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)); - } - - // Comments on the columns - foreach (var columnOp in operation.Columns.Where(c => c.Comment is not null)) - { - var columnComment = columnOp.Comment; - builder.AppendLine(";"); - - builder - .Append("COMMENT ON COLUMN ") - .Append(DelimitIdentifier(operation.Name, operation.Schema)) - .Append(".") - .Append(DelimitIdentifier(columnOp.Name)) - .Append(" IS ") - .Append(_stringTypeMapping.GenerateSqlLiteral(columnComment)); - } - - if (terminate) - { - builder.AppendLine(";"); - EndStatement(builder); - } - } - - /// - protected override void Generate(AlterTableOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - var alterTableBaseSql = $"ALTER TABLE {DelimitIdentifier(operation.Name, operation.Schema)}"; - var madeChanges = false; - - // Storage parameters - madeChanges |= AppendStorageParameterAlterations(operation.OldTable, operation, alterTableBaseSql, builder); - - // Comment - if (operation.Comment != operation.OldTable.Comment) - { - builder - .Append("COMMENT ON TABLE ") - .Append(DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" IS ") - .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)); - - builder.AppendLine(";"); - madeChanges = true; - } - - // Unlogged table (null is equivalent to false) - var oldUnlogged = operation.OldTable[NpgsqlAnnotationNames.UnloggedTable] is true; - var newUnlogged = operation[NpgsqlAnnotationNames.UnloggedTable] is true; - - if (oldUnlogged != newUnlogged) - { - builder - .Append(alterTableBaseSql) - .Append(" SET ") - .Append(newUnlogged ? "UNLOGGED" : "LOGGED") - .AppendLine(";"); - - madeChanges = true; - } - - if (madeChanges) - { - EndStatement(builder); - } - } - - /// - protected override void Generate( - DropColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - // Never touch system columns - if (IsSystemColumn(operation.Name)) - { - return; - } - - base.Generate(operation, model, builder, terminate); - } - - /// - protected override void Generate( - AddColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - if (!terminate && operation.Comment is not null) - { - throw new ArgumentException( - $"When generating migrations SQL for {nameof(AddColumnOperation)}, can't produce unterminated SQL with comments"); - } - - // Never touch system columns - if (IsSystemColumn(operation.Name)) - { - return; - } - - if (operation[NpgsqlAnnotationNames.ValueGenerationStrategy] is NpgsqlValueGenerationStrategy strategy) - { - switch (strategy) - { - case NpgsqlValueGenerationStrategy.SerialColumn: - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - // NB: This gets added to all added non-nullable columns by MigrationsModelDiffer. We need to suppress - // it, here because PG can't have both IDENTITY/SERIAL and a DEFAULT constraint on the same column. - operation.DefaultValue = null; - break; - } - } - - base.Generate(operation, model, builder, terminate: false); - - if (operation.Comment is not null) - { - builder.AppendLine(";"); - - builder - .Append("COMMENT ON COLUMN ") - .Append(DelimitIdentifier(operation.Table, operation.Schema)) - .Append(".") - .Append(DelimitIdentifier(operation.Name)) - .Append(" IS ") - .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)); - } - - if (terminate) - { - builder.AppendLine(";"); - EndStatement(builder); - } - } - - /// - protected override void Generate(AlterColumnOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - // Never touch system columns - if (IsSystemColumn(operation.Name)) - { - return; - } - - var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) - ?.Columns.FirstOrDefault(c => c.Name == operation.Name); - - ApplyTsVectorColumnSql(operation, model, operation.Name, operation.Schema, operation.Table); - - // Note: OldColumn doesn't have Schema, Table or Name populated (https://github.com/dotnet/efcore/issues/28041), so we take these - // from the new column (they're identical in any case). - ApplyTsVectorColumnSql(operation.OldColumn, model, operation.Name, operation.Schema, operation.Table); - - if (operation.ComputedColumnSql != operation.OldColumn.ComputedColumnSql - || operation.IsStored != operation.OldColumn.IsStored) - { - // TODO: The following will fail if the column being altered is part of an index. - // SqlServer recreates indexes, but wait to see if PostgreSQL will introduce a proper ALTER TABLE ALTER COLUMN - // that allows us to do this cleanly. - var dropColumnOperation = new DropColumnOperation - { - Schema = operation.Schema, - Table = operation.Table, - Name = operation.Name - }; - - if (column is not null) - { - dropColumnOperation.AddAnnotations(column.GetAnnotations()); - } - - Generate(dropColumnOperation, model, builder); - - var addColumnOperation = new AddColumnOperation - { - Schema = operation.Schema, - Table = operation.Table, - Name = operation.Name, - ClrType = operation.ClrType, - ColumnType = operation.ColumnType, - IsUnicode = operation.IsUnicode, - IsFixedLength = operation.IsFixedLength, - MaxLength = operation.MaxLength, - Precision = operation.Precision, - Scale = operation.Scale, - IsRowVersion = operation.IsRowVersion, - IsNullable = operation.IsNullable, - DefaultValue = operation.DefaultValue, - DefaultValueSql = operation.DefaultValueSql, - ComputedColumnSql = operation.ComputedColumnSql, - IsStored = operation.IsStored, - Comment = operation.Comment, - Collation = operation.Collation - }; - addColumnOperation.AddAnnotations(operation.GetAnnotations()); - Generate(addColumnOperation, model, builder); - RecreateIndexes(column, operation, builder); - builder.EndCommand(); - - return; - } - - string? newSequenceName = null; - - var alterBase = $"ALTER TABLE {DelimitIdentifier(operation.Table, operation.Schema)} " - + $"ALTER COLUMN {DelimitIdentifier(operation.Name)} "; - - // TYPE + COLLATION - var type = operation.ColumnType ?? GetColumnType(operation.Schema, operation.Table, operation.Name, operation, model)!; - var oldType = IsOldColumnSupported(model) - ? operation.OldColumn.ColumnType ?? GetColumnType(operation.Schema, operation.Table, operation.Name, operation.OldColumn, model) - : null; - - // If a collation was defined on the column specifically, via the standard EF mechanism, it will be - // available in operation.Collation (as usual). - // If not, there may be a model-wide default column collation, which gets transmitted via the Npgsql-specific annotation. - // 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 - var oldCollation = (string?)(operation.OldColumn.Collation ?? operation.OldColumn[NpgsqlAnnotationNames.DefaultColumnCollation]); - var newCollation = (string?)(operation.Collation ?? operation[NpgsqlAnnotationNames.DefaultColumnCollation]); -#pragma warning restore CS0618 - - if (type != oldType || newCollation != oldCollation) - { - builder - .Append(alterBase) - .Append("TYPE ") - .Append(type); - - if (newCollation is not null) - { - 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(";"); - } - - if (operation is { IsNullable: true, OldColumn.IsNullable: false }) - { - builder - .Append(alterBase) - .Append("DROP NOT NULL") - .AppendLine(";"); - } - else if (operation is { IsNullable: false, OldColumn.IsNullable: true }) - { - // The column is being made non-nullable. Generate an update statement before doing that, to convert any existing null values to - // the default value (otherwise PostgreSQL fails). - if (operation.DefaultValueSql is not null || operation.DefaultValue is not null) - { - string defaultValueSql; - if (operation.DefaultValueSql is not null) - { - defaultValueSql = operation.DefaultValueSql; - } - else - { - Check.DebugAssert(operation.DefaultValue is not null, "operation.DefaultValue is not null"); - - var typeMapping = (type != null - ? Dependencies.TypeMappingSource.FindMapping(operation.DefaultValue.GetType(), type) - : null) - ?? Dependencies.TypeMappingSource.GetMappingForValue(operation.DefaultValue); - - defaultValueSql = typeMapping.GenerateSqlLiteral(operation.DefaultValue); - } - - builder - .Append("UPDATE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" SET ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" = ") - .Append(defaultValueSql) - .Append(" WHERE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" IS NULL") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - builder - .Append(alterBase) - .Append("SET NOT NULL") - .AppendLine(";"); - } - - // Compression - var oldCompressionMethod = (string?)operation.OldColumn[NpgsqlAnnotationNames.CompressionMethod]; - var newCompressionMethod = (string?)operation[NpgsqlAnnotationNames.CompressionMethod]; - - if (newCompressionMethod != oldCompressionMethod) - { - builder - .Append(alterBase) - .Append("SET COMPRESSION ") - .Append(newCompressionMethod ?? "default"); - } - - CheckForOldValueGenerationAnnotation(operation); - - var oldStrategy = operation.OldColumn[NpgsqlAnnotationNames.ValueGenerationStrategy] as NpgsqlValueGenerationStrategy?; - var newStrategy = operation[NpgsqlAnnotationNames.ValueGenerationStrategy] as NpgsqlValueGenerationStrategy?; - - if (oldStrategy != newStrategy) - { - // We have a value generation strategy change - - if (oldStrategy == NpgsqlValueGenerationStrategy.SerialColumn) - { - // TODO: It would be better to actually select for the owned sequence. - // This would require plpgsql. - var sequence = DelimitIdentifier($"{operation.Table}_{operation.Name}_seq", operation.Schema); - switch (newStrategy) - { - case null: - // Drop the serial, converting the column to a regular int - builder.AppendLine($"DROP SEQUENCE {sequence} CASCADE;"); - break; - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - // Convert serial column to identity, maintaining the current sequence value - var identityTypeClause = newStrategy == NpgsqlValueGenerationStrategy.IdentityAlwaysColumn - ? "ALWAYS" - : "BY DEFAULT"; - var oldSequence = DelimitIdentifier($"{operation.Table}_{operation.Name}_old_seq", operation.Schema); - var oldSequenceWithoutSchema = DelimitIdentifier($"{operation.Table}_{operation.Name}_old_seq"); - builder - .AppendLine($"ALTER SEQUENCE {sequence} RENAME TO {oldSequenceWithoutSchema};") - .AppendLine($"{alterBase}DROP DEFAULT;") - .AppendLine($"{alterBase}ADD GENERATED {identityTypeClause} AS IDENTITY;") - // When generating idempotent scripts, migration DDL is enclosed in anonymous DO blocks, - // where PERFORM must be used instead of SELECT - .Append(Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) ? "PERFORM" : "SELECT") - .AppendLine($" * FROM setval('{sequence}', nextval('{oldSequence}'), false);") - .AppendLine($"DROP SEQUENCE {oldSequence};"); - break; - default: - throw new NotSupportedException($"Don't know how to migrate serial column to {newStrategy}"); - } - } - else if (oldStrategy.IsIdentity()) - { - switch (newStrategy) - { - case null: - // Drop the identity, converting the column to a regular int - builder.Append(alterBase).AppendLine("DROP IDENTITY;"); - break; - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - builder.Append(alterBase).AppendLine("SET GENERATED ALWAYS;"); - break; - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - builder.Append(alterBase).AppendLine("SET GENERATED BY DEFAULT;"); - break; - case NpgsqlValueGenerationStrategy.SerialColumn: - throw new NotSupportedException("Migrating from identity to serial isn't currently supported (and is a bad idea)"); - default: - throw new NotSupportedException($"Don't know how to migrate identity column to {newStrategy}"); - } - } - else if (oldStrategy is null) - { - switch (newStrategy) - { - case NpgsqlValueGenerationStrategy.IdentityAlwaysColumn: - case NpgsqlValueGenerationStrategy.IdentityByDefaultColumn: - builder.Append(alterBase).AppendLine("DROP DEFAULT;"); - builder.Append(alterBase).Append("ADD"); - IdentityDefinition(operation, builder); - builder.AppendLine(";"); - break; - case NpgsqlValueGenerationStrategy.SerialColumn: - switch (type) - { - case "integer": - case "int": - case "int4": - case "bigint": - case "int8": - case "smallint": - case "int2": - newSequenceName = $"{operation.Table}_{operation.Name}_seq"; - Generate( - new CreateSequenceOperation - { - Schema = operation.Schema, - Name = newSequenceName, - ClrType = operation.ClrType - }, model, builder); - - builder.Append(alterBase).Append("SET"); - DefaultValue(null, $@"nextval('{DelimitIdentifier(newSequenceName, operation.Schema)}')", type, builder); - builder.AppendLine(";"); - // Note: we also need to set the sequence ownership, this is done below after the ALTER COLUMN - break; - } - - break; - default: - throw new NotSupportedException($"Don't know how to apply value generation strategy {newStrategy}"); - } - } - } - - // Identity sequence options may have changed - if (oldStrategy.IsIdentity() && newStrategy.IsIdentity()) - { - var newSequenceOptions = IdentitySequenceOptionsData.Get(operation); - var oldSequenceOptions = IdentitySequenceOptionsData.Get(operation.OldColumn); - - if (newSequenceOptions.StartValue != oldSequenceOptions.StartValue) - { - var startValue = newSequenceOptions.StartValue ?? 1; - - builder - .Append(alterBase) - .Append("RESTART WITH ") - .Append(startValue.ToString(CultureInfo.InvariantCulture)) - .AppendLine(";"); - } - - if (newSequenceOptions.IncrementBy != oldSequenceOptions.IncrementBy) - { - builder - .Append(alterBase) - .Append("SET INCREMENT BY ") - .Append(newSequenceOptions.IncrementBy.ToString(CultureInfo.InvariantCulture)) - .AppendLine(";"); - } - - if (newSequenceOptions.MinValue != oldSequenceOptions.MinValue) - { - builder - .Append(alterBase) - .Append( - newSequenceOptions.MinValue is null - ? "SET NO MINVALUE" - : "SET MINVALUE " + newSequenceOptions.MinValue) - .AppendLine(";"); - } - - if (newSequenceOptions.MaxValue != oldSequenceOptions.MaxValue) - { - builder - .Append(alterBase) - .Append( - newSequenceOptions.MaxValue is null - ? "SET NO MAXVALUE" - : "SET MAXVALUE " + newSequenceOptions.MaxValue) - .AppendLine(";"); - } - - if (newSequenceOptions.IsCyclic != oldSequenceOptions.IsCyclic) - { - builder - .Append(alterBase) - .Append( - newSequenceOptions.IsCyclic - ? "SET CYCLE" - : "SET NO CYCLE") - .AppendLine(";"); - } - - if (newSequenceOptions.NumbersToCache != oldSequenceOptions.NumbersToCache) - { - builder - .Append(alterBase) - .Append("SET CACHE ") - .Append(newSequenceOptions.NumbersToCache.ToString(CultureInfo.InvariantCulture)) - .AppendLine(";"); - } - } - - // DEFAULT. - // Note that defaults values for value-generated columns (identity, serial) are managed above. This is - // only for regular columns with user-specified default settings. - if (newStrategy is null - && (operation.DefaultValueSql != operation.OldColumn.DefaultValueSql - || !Equals(operation.DefaultValue, operation.OldColumn.DefaultValue))) - { - builder.Append(alterBase); - if (operation.DefaultValue is not null || operation.DefaultValueSql is not null) - { - builder.Append("SET"); - DefaultValue(operation.DefaultValue, operation.DefaultValueSql, type, builder); - } - else - { - builder.Append("DROP DEFAULT"); - } - - builder.AppendLine(";"); - } - - // A sequence has been created because this column was altered to be a serial. - // Change the sequence's ownership. - if (newSequenceName is not null) - { - builder - .Append("ALTER SEQUENCE ") - .Append(DelimitIdentifier(newSequenceName, operation.Schema)) - .Append(" OWNED BY ") - .Append(DelimitIdentifier(operation.Table, operation.Schema)) - .Append(".") - .Append(DelimitIdentifier(operation.Name)) - .AppendLine(";"); - } - - // Comment - if (operation.Comment != operation.OldColumn.Comment) - { - builder - .Append("COMMENT ON COLUMN ") - .Append(DelimitIdentifier(operation.Table, operation.Schema)) - .Append(".") - .Append(DelimitIdentifier(operation.Name)) - .Append(" IS ") - .Append(_stringTypeMapping.GenerateSqlLiteral(operation.Comment)) - .AppendLine(";"); - } - - EndStatement(builder); - } - - /// - protected override void Generate(RenameIndexOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - if (operation.NewName is not null && operation.NewName != operation.Name) - { - Rename(operation.Schema, operation.Name, operation.NewName, "INDEX", builder); - } - - // N.B. indexes are always stored in the same schema as the table. - EndStatement(builder); - } - - /// - protected override void Generate(RenameSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - var name = operation.Name; - if (operation.NewName is not null && operation.NewName != operation.Name) - { - Rename(operation.Schema, operation.Name, operation.NewName, "SEQUENCE", builder); - - name = operation.NewName; - } - - if (operation.NewSchema is not null && operation.NewSchema != operation.Schema) - { - Transfer(operation.NewSchema, operation.Schema, name, "SEQUENCE", builder); - } - - 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) - { - // PostgreSQL has ALTER SEQUENCE ... RESTART WITH x, which resets the current sequence value but does not change its start value - // in the schema (so a subsequence RESTART without an argument resets it back to its original start value, not to x). - // It also has ALTER SEQUENCE ... STARTS WITH x, which resets the schema start value but not the current value. - // So we use both statements to reset both the current value and the schema value. - if (operation.StartValue.HasValue) - { - var longTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(long)); - - builder - .Append("ALTER SEQUENCE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" START WITH ") - .Append(longTypeMapping.GenerateSqlLiteral(operation.StartValue.Value)) - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - builder - .Append("ALTER SEQUENCE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" RESTART") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - else - { - builder - .Append("ALTER SEQUENCE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" RESTART") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - EndStatement(builder); - } - - /// - protected override void Generate(RenameTableOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - var name = operation.Name; - if (operation.NewName is not null && operation.NewName != operation.Name) - { - Rename(operation.Schema, operation.Name, operation.NewName, "TABLE", builder); - - name = operation.NewName; - } - - if (operation.NewSchema is not null && operation.NewSchema != operation.Schema) - { - Transfer(operation.NewSchema, operation.Schema, name, "TABLE", builder); - } - - EndStatement(builder); - } - - /// - protected override void Generate( - CreateIndexOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - builder.Append("CREATE "); - - if (operation.IsUnique) - { - builder.Append("UNIQUE "); - } - - builder.Append("INDEX "); - - var concurrently = operation[NpgsqlAnnotationNames.CreatedConcurrently] as bool? == true; - if (concurrently) - { - builder.Append("CONCURRENTLY "); - } - - builder - .Append(DelimitIdentifier(operation.Name)) - .Append(" ON ") - .Append(DelimitIdentifier(operation.Table, operation.Schema)); - - var method = operation[NpgsqlAnnotationNames.IndexMethod] as string; - if (method?.Length > 0) - { - builder.Append(" USING ").Append(method); - } - - var indexColumns = GetIndexColumns(operation); - - var columnsExpression = operation[NpgsqlAnnotationNames.TsVectorConfig] is string tsVectorConfig - ? ColumnsToTsVector(operation.Name, indexColumns.Select(i => i.Name), tsVectorConfig, model, operation.Schema, operation.Table) - : IndexColumnList(indexColumns, method); - - builder - .Append(" (") - .Append(columnsExpression) - .Append(")"); - - IndexOptions(operation, model, builder); - - if (terminate) - { - builder.AppendLine(";"); - // Concurrent indexes cannot be created within a transaction - EndStatement(builder, suppressTransaction: concurrently); - } - } - - /// - protected override void IndexOptions(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - if (_postgresVersion.AtLeast(11) && operation[NpgsqlAnnotationNames.IndexInclude] is string[] { Length: > 0 } includeColumns) - { - builder - .Append(" INCLUDE (") - .Append(ColumnList(includeColumns)) - .Append(")"); - } - - if (operation[NpgsqlAnnotationNames.NullsDistinct] is false) - { - builder.Append(" NULLS NOT DISTINCT"); - } - - AppendStoreParameters(operation, builder, withLeadingNewline: false); - - base.IndexOptions(operation, model, builder); - } - - /// - protected override void Generate(EnsureSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - if (operation.Name == "public") - { - return; - } - - // PostgreSQL has CREATE SCHEMA IF NOT EXISTS, but that requires CREATE privileges on the database even if the schema already - // exists. This blocks multi-tenant scenarios where the user has no database privileges. - // So we procedurally check if the schema exists instead, and create it if not. - var schemaName = operation.Name.Replace("'", "''"); - - // If we're generating an idempotent migration, we're already in a PL/PGSQL DO block; otherwise we need to start one. - if (!Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) - { - builder - .AppendLine(@"DO $EF$") - .AppendLine("BEGIN"); - } - - builder - .AppendLine($" IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = '{schemaName}') THEN") - .AppendLine($" CREATE SCHEMA {DelimitIdentifier(operation.Name)};") - .AppendLine(" END IF;"); - - if (!Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) - { - builder.AppendLine("END $EF$;"); - } - - EndStatement(builder); - } - - /// - protected virtual void Generate(NpgsqlCreateDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - builder - .Append("CREATE DATABASE ") - .Append(DelimitIdentifier(operation.Name)); - - if (!string.IsNullOrEmpty(operation.Template)) - { - builder - .AppendLine() - .Append("TEMPLATE ") - .Append(DelimitIdentifier(operation.Template)); - } - - if (!string.IsNullOrEmpty(operation.Tablespace)) - { - builder - .AppendLine() - .Append("TABLESPACE ") - .Append(DelimitIdentifier(operation.Tablespace)); - } - - if (!string.IsNullOrEmpty(operation.Collation)) - { - builder - .AppendLine() - .Append("LC_COLLATE ") - .Append(DelimitIdentifier(operation.Collation)); - } - - builder.AppendLine(";"); - - EndStatement(builder, suppressTransaction: true); - } - - /// - public virtual void Generate(NpgsqlDropDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - var dbName = DelimitIdentifier(operation.Name); - - if (_postgresVersion.AtLeast(13)) - { - builder.AppendLine($"DROP DATABASE {dbName} WITH (FORCE);"); - } - else - { - builder - .AppendLine($"REVOKE CONNECT ON DATABASE {dbName} FROM PUBLIC;") - .AppendLine($"SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname = '{operation.Name}';") - .EndCommand(suppressTransaction: true) - .AppendLine($"DROP DATABASE {dbName};"); - } - - EndStatement(builder, suppressTransaction: true); - } - - /// - protected override void Generate( - AlterDatabaseOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - if (operation.Collation != operation.OldDatabase.Collation) - { - throw new NotSupportedException("PostgreSQL does not support altering the collation on an existing database."); - } - - GenerateCollationStatements(operation, model, builder); - GenerateEnumStatements(operation, model, builder); - GenerateRangeStatements(operation, model, builder); - - foreach (var extension in operation.GetPostgresExtensions()) - { - GenerateCreateExtension(extension, model, builder); - } - - builder.EndCommand(); - } - - /// - protected virtual void GenerateCreateExtension( - PostgresExtension extension, - IModel? model, - MigrationCommandListBuilder builder) - { - var schema = extension.Schema ?? model?.GetDefaultSchema(); - - // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences - // and other database objects. However, it isn't aware of extensions, so we always ensure schema on enum creation. - if (schema is not null) - { - Generate(new EnsureSchemaOperation { Name = schema }, model, builder); - } - - builder - .Append("CREATE EXTENSION IF NOT EXISTS ") - .Append(DelimitIdentifier(extension.Name)); - - if (extension.Schema is not null) - { - builder - .Append(" SCHEMA ") - .Append(DelimitIdentifier(extension.Schema)); - } - - if (extension.Version is not null) - { - builder - .Append(" VERSION ") - .Append(DelimitIdentifier(extension.Version)); - } - - builder.AppendLine(" CASCADE;"); - } - - #region Collation management - - /// - protected virtual void GenerateCollationStatements(AlterDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - foreach (var collationToCreate in operation.GetPostgresCollations() - .Where(ne => operation.GetOldPostgresCollations().All(oe => oe.Name != ne.Name || oe.Schema != ne.Schema))) - { - GenerateCreateCollation(collationToCreate, model, builder); - } - - foreach (var collationToDrop in operation.GetOldPostgresCollations() - .Where(oe => operation.GetPostgresCollations().All(ne => ne.Name != oe.Name || oe.Schema != ne.Schema))) - { - GenerateDropCollation(collationToDrop, model, builder); - } - - foreach (var (newCollation, oldCollation) in operation.GetPostgresCollations() - .Join( - operation.GetOldPostgresCollations(), - e => new { e.Name, e.Schema }, - e => new { e.Name, e.Schema }, - (ne, oe) => (New: ne, Old: oe))) - { - if (newCollation.LcCollate != oldCollation.LcCollate - || newCollation.LcCtype != oldCollation.LcCtype - || newCollation.Provider != oldCollation.Provider - || newCollation.IsDeterministic != oldCollation.IsDeterministic) - { - throw new NotSupportedException("Altering an existing collation is not supported."); - } - } - } - - /// - protected virtual void GenerateCreateCollation(PostgresCollation collation, IModel? model, MigrationCommandListBuilder builder) - { - var schema = collation.Schema ?? model?.GetDefaultSchema(); - - // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences - // and other database objects. However, it isn't aware of collation, so we always ensure schema on collation creation. - if (schema is not null) - { - Generate(new EnsureSchemaOperation { Name = schema }, model, builder); - } - - builder - .Append("CREATE COLLATION ") - .Append(DelimitIdentifier(collation.Name, schema)) - .Append(" (") - .IncrementIndent(); - - var def = new List(); - - if (collation.LcCollate == collation.LcCtype) - { - def.Add($"LOCALE = {_stringTypeMapping.GenerateSqlLiteral(collation.LcCollate)}"); - } - else - { - def.Add($"LC_COLLATE = {_stringTypeMapping.GenerateSqlLiteral(collation.LcCollate)}"); - def.Add($"LC_CTYPE = {_stringTypeMapping.GenerateSqlLiteral(collation.LcCtype)}"); - } - - if (collation.Provider is not null) - { - def.Add($"PROVIDER = {collation.Provider}"); - } - - if (collation.IsDeterministic is not null) - { - def.Add($"DETERMINISTIC = {collation.IsDeterministic}"); - } - - for (var i = 0; i < def.Count; i++) - { - builder - .Append(def[i] + (i == def.Count - 1 ? null : ",")) - .AppendLine(); - } - - builder - .DecrementIndent() - .AppendLine(");"); - } - - /// - protected virtual void GenerateDropCollation(PostgresCollation collation, IModel? model, MigrationCommandListBuilder builder) - { - var schema = collation.Schema ?? model?.GetDefaultSchema(); - - builder - .Append("DROP COLLATION ") - .Append(DelimitIdentifier(collation.Name, schema)) - .AppendLine(";"); - } - - #endregion Collation management - - #region Enum management - - /// - protected virtual void GenerateEnumStatements(AlterDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - foreach (var enumTypeToCreate in operation.GetPostgresEnums() - .Where(ne => operation.GetOldPostgresEnums().All(oe => oe.Name != ne.Name || oe.Schema != ne.Schema))) - { - GenerateCreateEnum(enumTypeToCreate, model, builder); - } - - foreach (var enumTypeToDrop in operation.GetOldPostgresEnums() - .Where(oe => operation.GetPostgresEnums().All(ne => ne.Name != oe.Name || oe.Schema != ne.Schema))) - { - GenerateDropEnum(enumTypeToDrop, model, builder); - } - - foreach (var (newEnum, oldEnum) in operation.GetPostgresEnums().OrderBy(e => e.Schema).ThenBy(e => e.Name) - .Join( - 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))) - { - var (oldLabels, newLabels) = (oldEnum.Labels, newEnum.Labels); - - // We only support adding enum values - dropping is unsupported by PostgreSQL, and we don't want to - // go into rename detection heuristics (users can do that in raw SQL). - // See https://www.postgresql.org/docs/current/sql-altertype.html - - if (oldLabels.Except(newLabels).FirstOrDefault() is { } removedLabel) - { - throw new NotSupportedException( - $"Can't remove enum label '{removedLabel}' from enum type '{newEnum}'. " - + "Renaming a label is possible via a raw SQL migration (see " - + "https://www.postgresql.org/docs/current/sql-altertype.html)"); - } - - for (var newPos = 0; newPos < newLabels.Count; newPos++) - { - var newLabel = newLabels[newPos]; - if (oldLabels.Contains(newLabel)) - { - continue; - } - - // 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); - } - } - } - } - - /// - protected virtual void GenerateCreateEnum(PostgresEnum enumType, IModel? model, MigrationCommandListBuilder builder) - { - var schema = enumType.Schema ?? model?.GetDefaultSchema(); - - // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences - // and other database objects. However, it isn't aware of enums, so we always ensure schema on enum creation. - if (schema is not null) - { - Generate(new EnsureSchemaOperation { Name = schema }, model, builder); - } - - builder - .Append("CREATE TYPE ") - .Append(DelimitIdentifier(enumType.Name, schema)) - .Append(" AS ENUM ("); - - var labels = enumType.Labels; - for (var i = 0; i < labels.Count; i++) - { - builder.Append(_stringTypeMapping.GenerateSqlLiteral(labels[i])); - if (i < labels.Count - 1) - { - builder.Append(", "); - } - } - - builder.AppendLine(");"); - } - - /// - protected virtual void GenerateDropEnum(PostgresEnum enumType, IModel? model, MigrationCommandListBuilder builder) - { - var schema = enumType.Schema ?? model?.GetDefaultSchema(); - - builder - .Append("DROP TYPE ") - .Append(DelimitIdentifier(enumType.Name, schema)) - .AppendLine(";"); - } - - /// - 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 - .Append("ALTER TYPE ") - .Append(DelimitIdentifier(enumType.Name, schema)) - .Append(" ADD VALUE ") - .Append(_stringTypeMapping.GenerateSqlLiteral(addedLabel)); - - if (beforeLabel is not null) - { - builder - .Append(" BEFORE ") - .Append(_stringTypeMapping.GenerateSqlLiteral(beforeLabel)); - } - else if (afterLabel is not null) - { - builder - .Append(" AFTER ") - .Append(_stringTypeMapping.GenerateSqlLiteral(afterLabel)); - } - - builder.AppendLine(";"); - - // Adding an enum label cannot be done in a transaction prior to PG12 - if (_postgresVersion.IsUnder(12)) - { - EndStatement(builder, suppressTransaction: true); - } - } - - #endregion Enum management - - #region Range management - - /// - protected virtual void GenerateRangeStatements(AlterDatabaseOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - foreach (var rangeTypeToCreate in operation.GetPostgresRanges() - .Where(ne => operation.GetOldPostgresRanges().All(oe => oe.Name != ne.Name))) - { - GenerateCreateRange(rangeTypeToCreate, model, builder); - } - - foreach (var rangeTypeToDrop in operation.GetOldPostgresRanges() - .Where(oe => operation.GetPostgresRanges().All(ne => ne.Name != oe.Name))) - { - GenerateDropRange(rangeTypeToDrop, model, builder); - } - - if (operation.GetPostgresRanges().FirstOrDefault( - nr => - operation.GetOldPostgresRanges().Any(or => or.Name == nr.Name) - ) is { } rangeTypeToAlter) - { - throw new NotSupportedException($"Altering range type ${rangeTypeToAlter} isn't supported."); - } - } - - /// - protected virtual void GenerateCreateRange(PostgresRange rangeType, IModel? model, MigrationCommandListBuilder builder) - { - var schema = rangeType.Schema ?? model?.GetDefaultSchema(); - - // Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences - // and other database objects. However, it isn't aware of ranges, so we always ensure schema on range creation. - if (schema is not null) - { - Generate(new EnsureSchemaOperation { Name = schema }, model, builder); - } - - builder - .Append("CREATE TYPE ") - .Append(DelimitIdentifier(rangeType.Name, schema)) - .AppendLine(" AS RANGE (") - .IncrementIndent(); - - var def = new List { $"SUBTYPE = {rangeType.Subtype}" }; - if (rangeType.CanonicalFunction is not null) - { - def.Add($"CANONICAL = {rangeType.CanonicalFunction}"); - } - - if (rangeType.SubtypeOpClass is not null) - { - def.Add($"SUBTYPE_OPCLASS = {rangeType.SubtypeOpClass}"); - } - - if (rangeType.CanonicalFunction is not null) - { - def.Add($"COLLATION = {rangeType.Collation}"); - } - - if (rangeType.SubtypeDiff is not null) - { - def.Add($"SUBTYPE_DIFF = {rangeType.SubtypeDiff}"); - } - - for (var i = 0; i < def.Count; i++) - { - builder - .Append(def[i] + (i == def.Count - 1 ? null : ",")) - .AppendLine(); - } - - builder - .DecrementIndent() - .AppendLine(");"); - } - - /// - protected virtual void GenerateDropRange(PostgresRange rangeType, IModel? model, MigrationCommandListBuilder builder) - { - var schema = rangeType.Schema ?? model?.GetDefaultSchema(); - - builder - .Append("DROP TYPE ") - .Append(DelimitIdentifier(rangeType.Name, schema)) - .AppendLine(";"); - } - - #endregion Range management - - /// - protected override void Generate( - DropIndexOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - builder - .Append("DROP INDEX ") - .Append(DelimitIdentifier(operation.Name, operation.Schema)); - - if (terminate) - { - builder.AppendLine(";"); - EndStatement(builder); - } - } - - /// - protected override void Generate(RenameColumnOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - builder.Append("ALTER TABLE ") - .Append(DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" RENAME COLUMN ") - .Append(DelimitIdentifier(operation.Name)) - .Append(" TO ") - .Append(DelimitIdentifier(operation.NewName)) - .AppendLine(";"); - - EndStatement(builder); - } - - /// - /// Builds commands for the given by making calls on the given - /// , and then terminates the final command. - /// - /// The operation. - /// The target model which may be null if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - InsertDataOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - var sqlBuilder = new StringBuilder(); - foreach (var modificationCommand in GenerateModificationCommands(operation, model)) - { - var overridingSystemValue = modificationCommand.ColumnModifications.Any( - m => - m.Property?.GetValueGenerationStrategy() == NpgsqlValueGenerationStrategy.IdentityAlwaysColumn); - ((NpgsqlUpdateSqlGenerator)Dependencies.UpdateSqlGenerator).AppendInsertOperation( - sqlBuilder, - modificationCommand, - 0, - overridingSystemValue, - out _); - } - - builder.Append(sqlBuilder.ToString()); - - if (terminate) - { - builder.EndCommand(); - } - } - - /// - protected override void Generate(CreateSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - Check.NotNull(operation, nameof(operation)); - - if (_postgresVersion.AtLeast(10)) - { - base.Generate(operation, model, builder); - } - else - { - // "CREATE SEQUENCE name AS type" expression is supported only in PostgreSQL 10 or above. - // The base MigrationsSqlGenerator.Generate method generates that expression. - // https://github.com/aspnet/EntityFrameworkCore/blob/master/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs#L533-L535 - var oldValue = operation.ClrType; - operation.ClrType = typeof(long); - base.Generate(operation, model, builder); - operation.ClrType = oldValue; - } - } - - #endregion Standard migrations - - #region Utilities - - /// - protected override void ColumnDefinition( - string? schema, - string table, - string name, - ColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - Check.NotEmpty(name, nameof(name)); - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - if (operation.ColumnType is null) - { - operation.ColumnType = GetColumnType(schema, table, name, operation, model); - } - - CheckForOldValueGenerationAnnotation(operation); - var valueGenerationStrategy = operation[NpgsqlAnnotationNames.ValueGenerationStrategy] as NpgsqlValueGenerationStrategy?; - if (valueGenerationStrategy == NpgsqlValueGenerationStrategy.SerialColumn) - { - if (operation.IsNullable) - { - throw new NotSupportedException("SERIAL columns can't be nullable"); - } - - switch (operation.ColumnType) - { - case "int": - case "int4": - case "integer": - operation.ColumnType = "serial"; - break; - case "bigint": - case "int8": - operation.ColumnType = "bigserial"; - break; - case "smallint": - case "int2": - operation.ColumnType = "smallserial"; - break; - } - } - - ApplyTsVectorColumnSql(operation, model, operation.Name, schema, table); - - if (operation.ComputedColumnSql is not null) - { - ComputedColumnDefinition(schema, table, name, operation, model, builder); - - return; - } - - var columnType = operation.ColumnType ?? GetColumnType(schema, table, name, operation, model)!; - builder - .Append(DelimitIdentifier(name)) - .Append(" ") - .Append(columnType); - - if (operation[NpgsqlAnnotationNames.CompressionMethod] is string compressionMethod) - { - builder - .Append(" COMPRESSION ") - .Append(DelimitIdentifier(compressionMethod)); - } - - // If a collation was defined on the column specifically, via the standard EF mechanism, it will be - // available in operation.Collation (as usual). - // If not, there may be a model-wide default column collation, which gets transmitted via the Npgsql-specific annotation. - // 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 - var collation = (string?)(operation.Collation ?? operation[NpgsqlAnnotationNames.DefaultColumnCollation]); -#pragma warning restore CS0618 - if (collation is not null) - { - builder - .Append(" COLLATE ") - .Append(DelimitIdentifier(collation)); - } - - if (valueGenerationStrategy.IsIdentity()) - { - IdentityDefinition(operation, builder); - } - else - { - if (!operation.IsNullable) - { - builder.Append(" NOT NULL"); - } - - DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder); - } - } - - /// - protected override void DefaultValue( - object? defaultValue, - string? defaultValueSql, - string? columnType, - MigrationCommandListBuilder builder) - { - // This is a hacky workaround for https://github.com/dotnet/efcore/issues/32353 - the EF MigrationsModelDiffer generates an empty - // string as the default value for JSON columns, but that's not valid as a JSON document and rejected by PG's jsonb type. So we - // replace the empty string with an empty JSON document {}. - // Note that even after the EF-side issue is fixed, removing this hack is a breaking change as migrations have already been - // scaffolded with an empty string. - if (columnType is "jsonb" or "json" && defaultValue is "") - { - defaultValue = "{}"; - } - - base.DefaultValue(defaultValue, defaultValueSql, columnType, builder); - } - - /// - /// Checks for a annotation on the given column, and if found, assigns - /// the appropriate SQL to . - /// - protected virtual void ApplyTsVectorColumnSql(ColumnOperation column, IModel? model, string name, string? schema, string table) - { - if (column[NpgsqlAnnotationNames.TsVectorConfig] is string tsVectorConfig) - { - var tsVectorIncludedColumns = column[NpgsqlAnnotationNames.TsVectorProperties] as string[]; - if (tsVectorIncludedColumns is null) - { - throw new InvalidOperationException( - $"{nameof(NpgsqlAnnotationNames.TsVectorConfig)} is present in a migration but " - + $"{nameof(NpgsqlAnnotationNames.TsVectorProperties)} is absent or empty"); - } - - column.ComputedColumnSql = ColumnsToTsVector(name, tsVectorIncludedColumns, tsVectorConfig, model, schema, table); - column.IsStored = true; - - column.RemoveAnnotation(NpgsqlAnnotationNames.TsVectorConfig); - } - } - - // Note: this definition is only used for creating new identity columns, not for alterations. - /// - protected virtual void IdentityDefinition( - ColumnOperation operation, - MigrationCommandListBuilder builder) - { - if (operation[NpgsqlAnnotationNames.ValueGenerationStrategy] is not NpgsqlValueGenerationStrategy strategy - || !strategy.IsIdentity()) - { - return; - } - - builder.Append( - strategy == NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ? " GENERATED BY DEFAULT AS IDENTITY" - : " GENERATED ALWAYS AS IDENTITY"); - - // Handle sequence options for the identity column - if (operation[NpgsqlAnnotationNames.IdentityOptions] is string identitySequenceOptions) - { - // TODO: Potential for refactoring with regular sequences (i.e. calling SequenceOptions), - // but some complexity to be worked out around creating/altering/restarting - - var sequenceData = IdentitySequenceOptionsData.Deserialize(identitySequenceOptions); - - var optionsWritten = false; - - var incrementBy = sequenceData.IncrementBy; - - var defaultMinValue = incrementBy > 0 ? 1 : Min(operation.ClrType); - var defaultMaxValue = incrementBy > 0 ? Max(operation.ClrType) : -1; - - var minValue = sequenceData.MinValue ?? defaultMinValue; - var maxValue = sequenceData.MaxValue ?? defaultMaxValue; - - var defaultStartValue = incrementBy > 0 ? minValue : maxValue; - if (sequenceData.StartValue.HasValue && sequenceData.StartValue != defaultStartValue) - { - Append("START WITH " + sequenceData.StartValue); - } - - if (incrementBy != 1) - { - Append("INCREMENT BY " + incrementBy); - } - - if (minValue != defaultMinValue) - { - Append("MINVALUE " + minValue); - } - - if (maxValue != defaultMaxValue) - { - Append("MAXVALUE " + maxValue); - } - - if (sequenceData.IsCyclic) - { - Append("CYCLE"); - } - - if (sequenceData.NumbersToCache != 1) - { - Append("CACHE " + sequenceData.NumbersToCache); - } - - if (optionsWritten) - { - builder.Append(")"); - } - - void Append(string s) - { - builder - .Append(optionsWritten ? " " : " (") - .Append(s); - optionsWritten = true; - } - - // Note: in older versions of PostgreSQL there's a slight variation, see NpgsqlDatabaseModelFactory. - // This is currently only used by identity, which is only supported on PG 10 anyway. - long Min(Type type) - { - if (type == typeof(int)) - { - return int.MinValue; - } - - if (type == typeof(long)) - { - return long.MinValue; - } - - if (type == typeof(short)) - { - return short.MinValue; - } - - throw new ArgumentOutOfRangeException(); - } - - long Max(Type type) - { - if (type == typeof(int)) - { - return int.MaxValue; - } - - if (type == typeof(long)) - { - return long.MaxValue; - } - - if (type == typeof(short)) - { - return short.MaxValue; - } - - throw new ArgumentOutOfRangeException(); - } - } - } - - /// - /// Generates a SQL fragment for a computed column definition for the given column metadata. - /// - /// The schema that contains the table, or null to use the default schema. - /// The table that contains the column. - /// The column name. - /// The column metadata. - /// The target model which may be null if the operations exist without a model. - /// The command builder to use to add the SQL fragment. - protected override void ComputedColumnDefinition( - string? schema, - string table, - string name, - ColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - Check.NotEmpty(name, nameof(name)); - Check.NotNull(operation, nameof(operation)); - Check.NotNull(builder, nameof(builder)); - - if (_postgresVersion < new Version(12, 0)) - { - throw new NotSupportedException("Computed/generated columns aren't supported in PostgreSQL prior to version 12"); - } - - if (operation.IsStored is not true && _postgresVersion < new Version(18, 0)) - { - throw new NotSupportedException( - "Virtual (non-stored) generated columns are only supported on PostgreSQL 18 and up. " + - "On older versions, specify 'stored: true' in " - + $"'{nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql)}' in your context's OnModelCreating."); - } - - builder - .Append(DelimitIdentifier(name)) - .Append(" ") - .Append(operation.ColumnType ?? GetColumnType(schema, table, name, operation, model)!); - - if (operation.Collation is not null) - { - builder - .Append(" COLLATE ") - .Append(DelimitIdentifier(operation.Collation)); - } - - builder - .Append(" GENERATED ALWAYS AS (") - .Append(operation.ComputedColumnSql!) - .Append(")"); - - if (operation.IsStored is true) - { - builder.Append(" STORED"); - } - - if (!operation.IsNullable) - { - builder.Append(" NOT NULL"); - } - } - -#pragma warning disable 618 - // Version 1.0 had a bad strategy for expressing serial columns, which depended on a - // ValueGeneratedOnAdd annotation. Detect that and throw. - private static void CheckForOldValueGenerationAnnotation(IAnnotatable annotatable) - { - if (annotatable.FindAnnotation(NpgsqlAnnotationNames.ValueGeneratedOnAdd) is not null) - { - throw new NotSupportedException( - "The Npgsql:ValueGeneratedOnAdd annotation has been found in your migrations, but is no longer supported. Please replace it with '.Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.SerialColumn)' where you want PostgreSQL serial (autoincrement) columns, and remove it in all other cases."); - } - } -#pragma warning restore 618 - - /// - /// Renames a database object such as a table, index, or sequence. - /// - /// The current schema of the object to rename. - /// The current name of the object to rename. - /// The new name. - /// The type of the object (e.g. TABLE, INDEX, SEQUENCE). - /// The builder to which operations are appended. - public virtual void Rename( - string? schema, - string name, - string newName, - string type, - MigrationCommandListBuilder builder) - { - Check.NotEmpty(name, nameof(name)); - Check.NotEmpty(newName, nameof(newName)); - Check.NotEmpty(type, nameof(type)); - Check.NotNull(builder, nameof(builder)); - - builder - .Append("ALTER ") - .Append(type) - .Append(" ") - .Append(DelimitIdentifier(name, schema)) - .Append(" RENAME TO ") - .Append(DelimitIdentifier(newName)) - .AppendLine(";"); - } - - /// - /// Transfers a database object such as a table, index, or sequence between schemas. - /// - /// The new schema. - /// The current schema. - /// The name of the object to transfer. - /// The type of the object (e.g. TABLE, INDEX, SEQUENCE). - /// The builder to which operations are appended. - public virtual void Transfer( - string newSchema, - string? schema, - string name, - string type, - MigrationCommandListBuilder builder) - { - Check.NotEmpty(newSchema, nameof(newSchema)); - Check.NotEmpty(name, nameof(name)); - Check.NotNull(type, nameof(type)); - Check.NotNull(builder, nameof(builder)); - - builder - .Append("ALTER ") - .Append(type) - .Append(" ") - .Append(DelimitIdentifier(name, schema)) - .Append(" SET SCHEMA ") - .Append(DelimitIdentifier(newSchema)) - .AppendLine(";"); - } - - /// - protected virtual void RecreateIndexes(IColumn? column, MigrationOperation currentOperation, MigrationCommandListBuilder builder) - { - foreach (var index in GetIndexesToRebuild()) - { - Generate(CreateIndexOperation.CreateFrom(index), index.Table.Model.Model, builder, terminate: false); - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - IEnumerable GetIndexesToRebuild() - { - if (column == null) - { - yield break; - } - - var table = column.Table; - var createIndexOperations = _operations.SkipWhile(o => o != currentOperation).Skip(1) - .OfType().Where(o => o.Table == table.Name && o.Schema == table.Schema).ToList(); - foreach (var index in table.Indexes) - { - var indexName = index.Name; - if (createIndexOperations.Any(o => o.Name == indexName)) - { - continue; - } - - if (index.Columns.Any(c => c == column)) - { - yield return index; - } - else if (index[NpgsqlAnnotationNames.IndexInclude] is IReadOnlyList includeColumns - && includeColumns.Contains(column.Name)) - { - yield return index; - } - } - } - } - - #endregion Utilities - - #region System column utilities - - private bool IsSystemColumn(string name) - => name == "oid" && _postgresVersion.IsUnder(12) || SystemColumnNames.Contains(name); - - /// - /// Tables in PostgreSQL implicitly have a set of system columns, which are always there. - /// We want to allow users to access these columns (i.e. xmin for optimistic concurrency) but - /// they should never generate migration operations. - /// - /// - /// https://www.postgresql.org/docs/current/static/ddl-system-columns.html - /// - private static readonly string[] SystemColumnNames = ["tableoid", "xmin", "cmin", "xmax", "cmax", "ctid"]; - - #endregion System column utilities - - #region Storage parameter utilities - - private void AppendStoreParameters(Annotatable annotatable, MigrationCommandListBuilder builder, bool withLeadingNewline) - { - var storageParameters = GetStorageParameters(annotatable); - if (storageParameters.Count > 0) - { - if (withLeadingNewline) - { - builder.AppendLine(); - } - else - { - builder.Append(" "); - } - - builder - .Append("WITH (") - .Append(string.Join(", ", storageParameters.Select(p => $"{p.Key}={p.Value}"))) - .Append(")"); - } - } - - private Dictionary GetStorageParameters(Annotatable annotatable) - => annotatable.GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) - .ToDictionary( - a => a.Name.Substring(NpgsqlAnnotationNames.StorageParameterPrefix.Length), - a => a.Value switch - { - bool b => b ? "true" : "false", - string s => $"'{s}'", - _ => a.Value!.ToString()! - }); - - // TODO: Call this for AlterIndexOperation when that's added (https://github.com/dotnet/efcore/issues/20692) - private bool AppendStorageParameterAlterations( - Annotatable oldAnnotatable, - Annotatable newAnnotatable, - string alterBaseSql, - MigrationCommandListBuilder builder) - { - var madeChanges = false; - - var oldStorageParameters = GetStorageParameters(oldAnnotatable); - var newStorageParameters = GetStorageParameters(newAnnotatable); - - var newOrChanged = newStorageParameters.Where( - p => - !oldStorageParameters.ContainsKey(p.Key) || oldStorageParameters[p.Key] != p.Value) - .ToList(); - - if (newOrChanged.Count > 0) - { - builder - .Append(alterBaseSql) - .Append(" SET (") - .Append(string.Join(", ", newOrChanged.Select(p => $"{p.Key}={p.Value}"))) - .Append(")"); - - builder.AppendLine(";"); - madeChanges = true; - } - - var removed = oldStorageParameters - .Select(p => p.Key) - .Where(pn => !newStorageParameters.ContainsKey(pn)) - .ToList(); - - if (removed.Count > 0) - { - builder - .Append(alterBaseSql) - .Append(" RESET (") - .Append(string.Join(", ", removed)) - .Append(")"); - - builder.AppendLine(";"); - madeChanges = true; - } - - return madeChanges; - } - - #endregion Storage parameter utilities - - #region Helpers - - private string DelimitIdentifier(string identifier) - => Dependencies.SqlGenerationHelper.DelimitIdentifier(identifier); - - private string DelimitIdentifier(string name, string? schema) - => Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema); - - private string IndexColumnList(IndexColumn[] columns, string? method) - { - var builder = new StringBuilder(); - - for (var i = 0; i < columns.Length; i++) - { - var column = columns[i]; - - if (i > 0) - { - builder.Append(", "); - } - - 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) - ? DelimitIdentifier(name, schema) - : DelimitIdentifier(column.Operator); - - builder.Append(" ").Append(delimitedOperator); - } - - // 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") - { - if (column.IsDescending) - { - builder.Append(" DESC"); - } - - if (column.NullSortOrder != NullSortOrder.Unspecified) - { - builder.Append(" NULLS "); - - switch (column.NullSortOrder) - { - case NullSortOrder.NullsFirst: - builder.Append("FIRST"); - break; - case NullSortOrder.NullsLast: - builder.Append("LAST"); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - } - - return builder.ToString(); - } - - private string ColumnsToTsVector( - string columnOrIndexName, - IEnumerable columnNames, - string tsVectorConfig, - IModel? model, - string? schema, - string table) - { - var columns = columnNames - .Select(columnName => model?.GetRelationalModel().FindTable(table, schema)?.Columns.FirstOrDefault(c => c.Name == columnName)) - .ToArray(); - - IEnumerable> columnGroups = columns - .GroupBy( - c => c?.StoreType switch - { - "json" => "json", - "jsonb" => "jsonb", - null => "null", - _ => "text" - - // Note: we currently don't support array_to_tsvector since it doesn't accept a search configuration - })!; - - var tsVectorConfigLiteral = _stringTypeMapping.GenerateSqlLiteral(tsVectorConfig); - - var builder = new StringBuilder(); - - foreach (var columnGroup in columnGroups) - { - if (builder.Length > 0) - { - builder.Append(" || "); - } - - builder.Append( - columnGroup.Key switch - { - "text" => $"to_tsvector({tsVectorConfigLiteral}, {string.Join(" || ' ' || ", columnGroup.Select(TextColumn))})", - "json" => string.Join( - " || ", columnGroup.Select( - c => - $"""json_to_tsvector({tsVectorConfigLiteral}, {JsonColumn(c)}, '"all"')""")), - "jsonb" => string.Join( - " || ", columnGroup.Select( - c => - $"""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() - }); - } - - return builder.ToString(); - - string TextColumn(IColumn column) - => column.IsNullable ? $"coalesce({DelimitIdentifier(column.Name)}, '')" : DelimitIdentifier(column.Name); - - string JsonColumn(IColumn column) - => column.IsNullable ? $"coalesce({DelimitIdentifier(column.Name)}, '{{}}')" : DelimitIdentifier(column.Name); - } - - private static bool TryParseSchema(string identifier, out string name, out string? schema) - { - var index = identifier.IndexOf('.'); - - if (index >= 0) - { - schema = identifier.Substring(0, index); - name = identifier.Substring(index + 1); - return true; - } - - name = identifier; - schema = default; - return false; - } - - private static IndexColumn[] GetIndexColumns(CreateIndexOperation operation) - { - var collations = operation[RelationalAnnotationNames.Collation] as string[]; - - var operators = operation[NpgsqlAnnotationNames.IndexOperators] as string[]; - - // We used to have our own annotation-based descending index mechanism, this got replaced with IsDescending in EF Core 7.0. - var isDescendingValues = operation.IsDescending; - var legacySortOrders = operation[NpgsqlAnnotationNames.IndexSortOrder] as SortOrder[]; - - var nullSortOrders = operation[NpgsqlAnnotationNames.IndexNullSortOrder] as NullSortOrder[]; - - var columns = new IndexColumn[operation.Columns.Length]; - - for (var i = 0; i < columns.Length; i++) - { - var name = operation.Columns[i]; - var @operator = i < operators?.Length ? operators[i] : null; - var collation = i < collations?.Length ? collations[i] : null; - var isColumnDescending = isDescendingValues is not null - ? isDescendingValues.Length == 0 || isDescendingValues[i] - : i < legacySortOrders?.Length && legacySortOrders[i] == SortOrder.Descending; - var nullSortOrder = i < nullSortOrders?.Length ? nullSortOrders[i] : NullSortOrder.Unspecified; - - columns[i] = new IndexColumn(name, @operator, collation, isColumnDescending, nullSortOrder); - } - - return columns; - } - - private readonly struct IndexColumn(string name, string? @operator, string? collation, bool isDescending, NullSortOrder nullSortOrder) - { - 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/Migrations/Operations/NpgsqlCreateDatabaseOperation.cs b/src/EFCore.PG/Migrations/Operations/NpgsqlCreateDatabaseOperation.cs deleted file mode 100644 index 88d92e5253..0000000000 --- a/src/EFCore.PG/Migrations/Operations/NpgsqlCreateDatabaseOperation.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; - -/// -/// A PostgreSQL-specific to create a database. -/// -/// -/// See Database migrations. -/// -[DebuggerDisplay("CREATE DATABASE {Name}")] -public class NpgsqlCreateDatabaseOperation : DatabaseOperation -{ - /// - /// The name of the database. - /// - public virtual string Name { get; set; } = null!; - - /// - /// The PostgreSQL database to use as a template for the new database to be created. - /// - public virtual string? Template { get; set; } - - /// - /// The PostgreSQL tablespace in which to create the database. - /// - public virtual string? Tablespace { get; set; } -} diff --git a/src/EFCore.PG/Migrations/Operations/NpgsqlDropDatabaseOperation.cs b/src/EFCore.PG/Migrations/Operations/NpgsqlDropDatabaseOperation.cs deleted file mode 100644 index 66184165a0..0000000000 --- a/src/EFCore.PG/Migrations/Operations/NpgsqlDropDatabaseOperation.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; - -/// -/// A PostgreSQL-specific to drop a database. -/// -/// -/// See Database migrations. -/// -public class NpgsqlDropDatabaseOperation : MigrationOperation -{ - /// - /// The name of the database. - /// - public virtual string Name { get; set; } = null!; -} diff --git a/src/EFCore.PG/NpgsqlRetryingExecutionStrategy.cs b/src/EFCore.PG/NpgsqlRetryingExecutionStrategy.cs deleted file mode 100644 index 00af219be1..0000000000 --- a/src/EFCore.PG/NpgsqlRetryingExecutionStrategy.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -/// -/// An implementation for retrying failed executions on PostgreSQL. -/// -/// -/// -/// The service lifetime is . This means that each instance will use -/// its own instance of this service. The implementation may depend on other services registered with any lifetime. The -/// implementation does not need to be thread-safe. -/// -/// -/// See Connection resiliency and database retries for more -/// information and examples. -/// -/// -public class NpgsqlRetryingExecutionStrategy : ExecutionStrategy -{ - private readonly ICollection? _additionalErrorCodes; - - /// - /// Creates a new instance of . - /// - /// The context on which the operations will be invoked. - /// - /// The default retry limit is 6, which means that the total amount of time spent before failing is about a minute. - /// - public NpgsqlRetryingExecutionStrategy( - DbContext context) - : this(context, DefaultMaxRetryCount) - { - } - - /// - /// Creates a new instance of . - /// - /// Parameter object containing service dependencies. - public NpgsqlRetryingExecutionStrategy( - ExecutionStrategyDependencies dependencies) - : this(dependencies, DefaultMaxRetryCount) - { - } - - /// - /// Creates a new instance of . - /// - /// The context on which the operations will be invoked. - /// The maximum number of retry attempts. - public NpgsqlRetryingExecutionStrategy( - DbContext context, - int maxRetryCount) - : this(context, maxRetryCount, DefaultMaxDelay, errorCodesToAdd: null) - { - } - - /// - /// Creates a new instance of . - /// - /// Parameter object containing service dependencies. - /// The maximum number of retry attempts. - public NpgsqlRetryingExecutionStrategy( - ExecutionStrategyDependencies dependencies, - int maxRetryCount) - : this(dependencies, maxRetryCount, DefaultMaxDelay, errorCodesToAdd: null) - { - } - - /// - /// Creates a new instance of . - /// - /// Parameter object containing service dependencies. - /// Additional error codes that should be considered transient. - public NpgsqlRetryingExecutionStrategy( - ExecutionStrategyDependencies dependencies, - ICollection? errorCodesToAdd) - : this(dependencies, DefaultMaxRetryCount, DefaultMaxDelay, errorCodesToAdd) - { - } - - /// - /// Creates a new instance of . - /// - /// The context on which the operations will be invoked. - /// The maximum number of retry attempts. - /// The maximum delay between retries. - /// Additional error codes that should be considered transient. - public NpgsqlRetryingExecutionStrategy( - DbContext context, - int maxRetryCount, - TimeSpan maxRetryDelay, - ICollection? errorCodesToAdd) - : base( - context, - maxRetryCount, - maxRetryDelay) - { - _additionalErrorCodes = errorCodesToAdd; - } - - /// - /// Creates a new instance of . - /// - /// Parameter object containing service dependencies. - /// The maximum number of retry attempts. - /// The maximum delay between retries. - /// Additional SQL error numbers that should be considered transient. - public NpgsqlRetryingExecutionStrategy( - ExecutionStrategyDependencies dependencies, - int maxRetryCount, - TimeSpan maxRetryDelay, - ICollection? errorCodesToAdd) - : base(dependencies, maxRetryCount, maxRetryDelay) - { - _additionalErrorCodes = errorCodesToAdd; - } - - // TODO: Unlike SqlException, which seems to also wrap various transport/IO errors - // and expose them via error codes, we have NpgsqlException with an inner exception. - // Would be good to provide a way to add these into the additional list. - /// - protected override bool ShouldRetryOn(Exception? exception) - => exception is PostgresException postgresException && _additionalErrorCodes?.Contains(postgresException.SqlState) == true - || NpgsqlTransientExceptionDetector.ShouldRetryOn(exception); -} diff --git a/src/EFCore.PG/Properties/AssemblyInfo.cs b/src/EFCore.PG/Properties/AssemblyInfo.cs deleted file mode 100644 index 54a68475e8..0000000000 --- a/src/EFCore.PG/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: DesignTimeProviderServices("Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal.NpgsqlDesignTimeServices")] - -[assembly: - InternalsVisibleTo( - "Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests, PublicKey=" - + "0024000004800000940000000602000000240000525341310004000001000100" - + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" - + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" - + "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" - + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] - -[assembly: - InternalsVisibleTo( - "Npgsql.EntityFrameworkCore.PostgreSQL.Tests, PublicKey=" - + "0024000004800000940000000602000000240000525341310004000001000100" - + "2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" - + "8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" - + "7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" - + "29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")] diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs deleted file mode 100644 index 23e9455d75..0000000000 --- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs +++ /dev/null @@ -1,607 +0,0 @@ -// - -#nullable enable - -using System.Resources; -using Microsoft.EntityFrameworkCore.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlStrings - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Npgsql.EntityFrameworkCore.PostgreSQL.Properties.NpgsqlStrings", typeof(NpgsqlStrings).Assembly); - - /// - /// 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 DataSourceAndConfigNotSupported - => GetString("DataSourceAndConfigNotSupported"); - - /// - /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different compression methods. - /// - public static string DuplicateColumnCompressionMethodMismatch(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table) - => string.Format( - GetString("DuplicateColumnCompressionMethodMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table)), - entityType1, property1, entityType2, property2, columnName, table); - - /// - /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different value generation strategies. - /// - public static string DuplicateColumnNameValueGenerationStrategyMismatch(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table) - => string.Format( - GetString("DuplicateColumnNameValueGenerationStrategyMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table)), - entityType1, property1, entityType2, property2, columnName, table); - - /// - /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different collation configurations. - /// - public static string DuplicateIndexCollationMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName) - => string.Format( - GetString("DuplicateIndexCollationMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), - index1, entityType1, index2, entityType2, table, indexName); - - /// - /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different concurrent creation configurations. - /// - public static string DuplicateIndexConcurrentCreationMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName) - => string.Format( - GetString("DuplicateIndexConcurrentCreationMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), - index1, entityType1, index2, entityType2, table, indexName); - - /// - /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different included columns: {includedColumns1} and {includedColumns2}. - /// - public static string DuplicateIndexIncludedMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName, object? includedColumns1, object? includedColumns2) - => string.Format( - GetString("DuplicateIndexIncludedMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName), nameof(includedColumns1), nameof(includedColumns2)), - index1, entityType1, index2, entityType2, table, indexName, includedColumns1, includedColumns2); - - /// - /// The EF Core 7.0 JSON support isn't currently supported by the Npgsql provider. To map to JSON, see https://www.npgsql.org/efcore/mapping/json.html. - /// - public static string Ef7JsonMappingNotSupported - => GetString("Ef7JsonMappingNotSupported"); - - /// - /// The 'FreeText' method is not supported because the query has switched to client-evaluation. Inspect the log to determine which query expressions are triggering client-evaluation. - /// - public static string FreeTextFunctionOnClient - => GetString("FreeTextFunctionOnClient"); - - /// - /// Heterogeneous store types detected when making new array ({type1}, {type2}). - /// - public static string HeterogeneousTypesInNewArray(object? type1, object? type2) - => string.Format( - GetString("HeterogeneousTypesInNewArray", nameof(type1), nameof(type2)), - type1, type2); - - /// - /// Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with signed integer properties. - /// - public static string IdentityBadType(object? property, object? entityType, object? propertyType) - => string.Format( - GetString("IdentityBadType", nameof(property), nameof(entityType), nameof(propertyType)), - property, entityType, propertyType); - - /// - /// Include property '{entityType}.{property}' cannot be defined multiple times - /// - public static string IncludePropertyDuplicated(object? entityType, object? property) - => string.Format( - GetString("IncludePropertyDuplicated", nameof(entityType), nameof(property)), - entityType, property); - - /// - /// Include property '{entityType}.{property}' is already included in the index - /// - public static string IncludePropertyInIndex(object? entityType, object? property) - => string.Format( - GetString("IncludePropertyInIndex", nameof(entityType), nameof(property)), - entityType, property); - - /// - /// Include property '{entityType}.{property}' not found - /// - public static string IncludePropertyNotFound(object? entityType, object? property) - => string.Format( - GetString("IncludePropertyNotFound", nameof(entityType), nameof(property)), - entityType, property); - - /// - /// The specified table '{table}' is not valid. Specify tables using the format '[schema].[table]'. - /// - public static string InvalidTableToIncludeInScaffolding(object? table) - => string.Format( - GetString("InvalidTableToIncludeInScaffolding", nameof(table)), - table); - - /// - /// The property '{property}' on entity type '{entityType}' is configured to use 'SequenceHiLo' value generator, which is only intended for keys. If this was intentional configure an alternate key on the property, otherwise call 'ValueGeneratedNever' or configure store generation for this property. - /// - public static string NonKeyValueGeneration(object? property, object? entityType) - => string.Format( - GetString("NonKeyValueGeneration", nameof(property), nameof(entityType)), - property, entityType); - - /// - /// Row values comparisons require two tuple arguments of the same length. - /// - public static string RowValueComparisonRequiresTuplesOfSameLength - => GetString("RowValueComparisonRequiresTuplesOfSameLength"); - - /// - /// Cannot set ProvideClientCertificatesCallback, RemoteCertificateValidationCallback or ProvidePasswordCallback when a data source is provided. - /// - public static string CannotUseDataSourceWithAuthCallbacks - => GetString("CannotUseDataSourceWithAuthCallbacks"); - - /// - /// PostgreSQL sequences cannot be used to generate values for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Sequences can only be used with integer properties. - /// - public static string SequenceBadType(object? property, object? entityType, object? propertyType) - => string.Format( - GetString("SequenceBadType", nameof(property), nameof(entityType), nameof(propertyType)), - property, entityType, propertyType); - - /// - /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support result columns; use output parameters instead. - /// - public static string StoredProcedureResultColumnsNotSupported(object? entityType, object? sproc) - => string.Format( - GetString("StoredProcedureResultColumnsNotSupported", nameof(entityType), nameof(sproc)), - entityType, sproc); - - /// - /// 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. - /// - public static string StoredProcedureReturnValueNotSupported(object? entityType, object? sproc) - => string.Format( - 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. - /// - public static string TransientExceptionDetected - => GetString("TransientExceptionDetected"); - - private static string GetString(string name, params string[] formatterNames) - { - var value = _resourceManager.GetString(name)!; - for (var i = 0; i < formatterNames.Length; i++) - { - value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); - } - - return value; - } - } -} - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.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 NpgsqlResources - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Npgsql.EntityFrameworkCore.PostgreSQL.Properties.NpgsqlStrings", typeof(NpgsqlResources).Assembly); - - /// - /// Enum column '{name}' cannot be scaffolded, define a CLR enum type and add the property manually. - /// - public static EventDefinition LogEnumColumnSkipped(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogEnumColumnSkipped; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogEnumColumnSkipped, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.EnumColumnSkippedWarning, - LogLevel.Warning, - "NpgsqlEfEventId.EnumColumnSkippedWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.EnumColumnSkippedWarning, - _resourceManager.GetString("LogEnumColumnSkipped")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Expression index '{name}' on table {tableName} cannot be scaffolded, expression indices aren't supported and must be added via raw SQL in migrations. - /// - public static EventDefinition LogExpressionIndexSkipped(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogExpressionIndexSkipped; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogExpressionIndexSkipped, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.ExpressionIndexSkippedWarning, - LogLevel.Warning, - "NpgsqlEfEventId.ExpressionIndexSkippedWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.ExpressionIndexSkippedWarning, - _resourceManager.GetString("LogExpressionIndexSkipped")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Found collation with name: {collationName}, schema: {schema}, LC_COLLATE: {lcCollate}, LC_CTYPE: {lcCtype}, provider: {provider}, deterministic: {isDeterministic} - /// - public static EventDefinition LogFoundCollation(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundCollation; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundCollation, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.CollationFound, - LogLevel.Debug, - "NpgsqlEfEventId.CollationFound", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.CollationFound, - _resourceManager.GetString("LogFoundCollation")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Found column with table: {tableName}, column name: {columnName}, data type: {dataType}, nullable: {isNullable}, identity: {isIdentity}, default value: {defaultValue}, computed value: {computedValue} - /// - public static FallbackEventDefinition LogFoundColumn(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundColumn; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundColumn, - logger, - static logger => new FallbackEventDefinition( - logger.Options, - NpgsqlEfEventId.ColumnFound, - LogLevel.Debug, - "NpgsqlEfEventId.ColumnFound", - _resourceManager.GetString("LogFoundColumn")!)); - } - - return (FallbackEventDefinition)definition; - } - - /// - /// Found foreign key on table: {tableName}, name: {foreignKeyName}, principal table: {principalTableName}, delete action: {deleteAction}. - /// - public static EventDefinition LogFoundForeignKey(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundForeignKey; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundForeignKey, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.ForeignKeyFound, - LogLevel.Debug, - "NpgsqlEfEventId.ForeignKeyFound", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.ForeignKeyFound, - _resourceManager.GetString("LogFoundForeignKey")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Found index with name: {indexName}, table: {tableName}, is unique: {isUnique}. - /// - public static EventDefinition LogFoundIndex(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundIndex; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundIndex, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.IndexFound, - LogLevel.Debug, - "NpgsqlEfEventId.IndexFound", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.IndexFound, - _resourceManager.GetString("LogFoundIndex")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Found primary key with name: {primaryKeyName}, table: {tableName}. - /// - public static EventDefinition LogFoundPrimaryKey(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundPrimaryKey; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundPrimaryKey, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.PrimaryKeyFound, - LogLevel.Debug, - "NpgsqlEfEventId.PrimaryKeyFound", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.PrimaryKeyFound, - _resourceManager.GetString("LogFoundPrimaryKey")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Found sequence name: {name}, data type: {dataType}, cyclic: {isCyclic}, increment: {increment}, start: {start}, minimum: {min}, maximum: {max}. - /// - public static FallbackEventDefinition LogFoundSequence(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundSequence; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundSequence, - logger, - static logger => new FallbackEventDefinition( - logger.Options, - NpgsqlEfEventId.SequenceFound, - LogLevel.Debug, - "NpgsqlEfEventId.SequenceFound", - _resourceManager.GetString("LogFoundSequence")!)); - } - - return (FallbackEventDefinition)definition; - } - - /// - /// Found table with name: {name}. - /// - public static EventDefinition LogFoundTable(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundTable; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundTable, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.TableFound, - LogLevel.Debug, - "NpgsqlEfEventId.TableFound", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.TableFound, - _resourceManager.GetString("LogFoundTable")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Found unique constraint with name: {uniqueConstraintName}, table: {tableName}. - /// - public static EventDefinition LogFoundUniqueConstraint(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundUniqueConstraint; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogFoundUniqueConstraint, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.UniqueConstraintFound, - LogLevel.Debug, - "NpgsqlEfEventId.UniqueConstraintFound", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.UniqueConstraintFound, - _resourceManager.GetString("LogFoundUniqueConstraint")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Unable to find a schema in the database matching the selected schema {schema}. - /// - public static EventDefinition LogMissingSchema(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogMissingSchema; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogMissingSchema, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.MissingSchemaWarning, - LogLevel.Warning, - "NpgsqlEfEventId.MissingSchemaWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.MissingSchemaWarning, - _resourceManager.GetString("LogMissingSchema")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Unable to find a table in the database matching the selected table {table}. - /// - public static EventDefinition LogMissingTable(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogMissingTable; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogMissingTable, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.MissingTableWarning, - LogLevel.Warning, - "NpgsqlEfEventId.MissingTableWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.MissingTableWarning, - _resourceManager.GetString("LogMissingTable")!))); - } - - return (EventDefinition)definition; - } - - /// - /// For foreign key {foreignKeyName} on table {tableName}, unable to find the column called {principalColumnName} on the foreign key's principal table, {principaltableName}. Skipping foreign key. - /// - public static EventDefinition LogPrincipalColumnNotFound(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogPrincipalColumnNotFound; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogPrincipalColumnNotFound, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.ForeignKeyPrincipalColumnMissingWarning, - LogLevel.Warning, - "NpgsqlEfEventId.ForeignKeyPrincipalColumnMissingWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.ForeignKeyPrincipalColumnMissingWarning, - _resourceManager.GetString("LogPrincipalColumnNotFound")!))); - } - - return (EventDefinition)definition; - } - - /// - /// For foreign key {fkName} on table {tableName}, unable to model the end of the foreign key on principal table {principaltableName}. This is usually because the principal table was not included in the selection set. - /// - public static EventDefinition LogPrincipalTableNotInSelectionSet(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogPrincipalTableNotInSelectionSet; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogPrincipalTableNotInSelectionSet, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.ForeignKeyReferencesMissingPrincipalTableWarning, - LogLevel.Warning, - "NpgsqlEfEventId.ForeignKeyReferencesMissingPrincipalTableWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.ForeignKeyReferencesMissingPrincipalTableWarning, - _resourceManager.GetString("LogPrincipalTableNotInSelectionSet")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Constraint '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). - /// - public static EventDefinition LogUnsupportedColumnConstraintSkipped(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogUnsupportedColumnConstraintSkipped; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogUnsupportedColumnConstraintSkipped, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.UnsupportedColumnConstraintSkippedWarning, - LogLevel.Warning, - "NpgsqlEfEventId.UnsupportedColumnConstraintSkippedWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.UnsupportedColumnConstraintSkippedWarning, - _resourceManager.GetString("LogUnsupportedColumnConstraintSkipped")!))); - } - - return (EventDefinition)definition; - } - - /// - /// Index '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). - /// - public static EventDefinition LogUnsupportedColumnIndexSkipped(IDiagnosticsLogger logger) - { - var definition = ((NpgsqlLoggingDefinitions)logger.Definitions).LogUnsupportedColumnIndexSkipped; - if (definition is null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((NpgsqlLoggingDefinitions)logger.Definitions).LogUnsupportedColumnIndexSkipped, - logger, - static logger => new EventDefinition( - logger.Options, - NpgsqlEfEventId.UnsupportedColumnIndexSkippedWarning, - LogLevel.Warning, - "NpgsqlEfEventId.UnsupportedColumnIndexSkippedWarning", - level => LoggerMessage.Define( - level, - NpgsqlEfEventId.UnsupportedColumnIndexSkippedWarning, - _resourceManager.GetString("LogUnsupportedColumnIndexSkipped")!))); - } - - return (EventDefinition)definition; - } - } -} - diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.tt b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.tt deleted file mode 100644 index e227dd0695..0000000000 --- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.tt +++ /dev/null @@ -1,5 +0,0 @@ -<# - Session["ResourceFile"] = "NpgsqlStrings.resx"; - Session["LoggingDefinitionsClass"] = "Diagnostics.Internal.NpgsqlLoggingDefinitions"; -#> -<#@ include file="..\..\..\tools\Resources.tt" #> diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.resx b/src/EFCore.PG/Properties/NpgsqlStrings.resx deleted file mode 100644 index d40cedf4e9..0000000000 --- a/src/EFCore.PG/Properties/NpgsqlStrings.resx +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 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. - - - '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different value generation strategies. - - - The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different collation configurations. - - - The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different concurrent creation configurations. - - - The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different included columns: {includedColumns1} and {includedColumns2}. - - - The EF Core 7.0 JSON support isn't currently supported by the Npgsql provider. To map to JSON, see https://www.npgsql.org/efcore/mapping/json.html. - - - The 'FreeText' method is not supported because the query has switched to client-evaluation. Inspect the log to determine which query expressions are triggering client-evaluation. - - - Heterogeneous store types detected when making new array ({type1}, {type2}). - - - Include property '{entityType}.{property}' cannot be defined multiple times - - - Include property '{entityType}.{property}' is already included in the index - - - Include property '{entityType}.{property}' not found - - - The specified table '{table}' is not valid. Specify tables using the format '[schema].[table]'. - - - Enum column '{name}' cannot be scaffolded, define a CLR enum type and add the property manually. - Warning NpgsqlEventId.EnumColumnSkippedWarning string - - - Expression index '{name}' on table {tableName} cannot be scaffolded, expression indices aren't supported and must be added via raw SQL in migrations. - Warning NpgsqlEventId.ExpressionIndexSkippedWarning string string - - - Found collation with name: {collationName}, schema: {schema}, LC_COLLATE: {lcCollate}, LC_CTYPE: {lcCtype}, provider: {provider}, deterministic: {isDeterministic} - Debug NpgsqlEventId.CollationFound string string string string string? bool - - - Found column with table: {tableName}, column name: {columnName}, data type: {dataType}, nullable: {isNullable}, identity: {isIdentity}, default value: {defaultValue}, computed value: {computedValue} - Debug NpgsqlEventId.ColumnFound string string string bool bool string string - - - Found foreign key on table: {tableName}, name: {foreignKeyName}, principal table: {principalTableName}, delete action: {deleteAction}. - Debug NpgsqlEventId.ForeignKeyFound string string string string - - - Found index with name: {indexName}, table: {tableName}, is unique: {isUnique}. - Debug NpgsqlEventId.IndexFound string string bool - - - Found primary key with name: {primaryKeyName}, table: {tableName}. - Debug NpgsqlEventId.PrimaryKeyFound string string - - - Found sequence name: {name}, data type: {dataType}, cyclic: {isCyclic}, increment: {increment}, start: {start}, minimum: {min}, maximum: {max}. - Debug NpgsqlEventId.SequenceFound string string bool int long long long - - - Found table with name: {name}. - Debug NpgsqlEventId.TableFound string - - - Found unique constraint with name: {uniqueConstraintName}, table: {tableName}. - Debug NpgsqlEventId.UniqueConstraintFound string? string - - - Unable to find a schema in the database matching the selected schema {schema}. - Warning NpgsqlEventId.MissingSchemaWarning string? - - - Unable to find a table in the database matching the selected table {table}. - Warning NpgsqlEventId.MissingTableWarning string? - - - For foreign key {foreignKeyName} on table {tableName}, unable to find the column called {principalColumnName} on the foreign key's principal table, {principaltableName}. Skipping foreign key. - Warning NpgsqlEventId.ForeignKeyPrincipalColumnMissingWarning string string string string - - - For foreign key {fkName} on table {tableName}, unable to model the end of the foreign key on principal table {principaltableName}. This is usually because the principal table was not included in the selection set. - Warning NpgsqlEventId.ForeignKeyReferencesMissingPrincipalTableWarning string? string? string? - - - Constraint '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). - Warning NpgsqlEventId.UnsupportedColumnConstraintSkippedWarning string? string - - - Index '{name}' on table {tableName} cannot be scaffolded because it includes a column that cannot be scaffolded (e.g. enum). - Warning NpgsqlEventId.UnsupportedColumnIndexSkippedWarning string string - - - The property '{property}' on entity type '{entityType}' is configured to use 'SequenceHiLo' value generator, which is only intended for keys. If this was intentional configure an alternate key on the property, otherwise call 'ValueGeneratedNever' or configure store generation for this property. - - - PostgreSQL sequences cannot be used to generate values for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Sequences can only be used with integer properties. - - - 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. - - - Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with signed integer properties. - - - Row values comparisons require two tuple arguments of the same length. - - - Cannot set ProvideClientCertificatesCallback, RemoteCertificateValidationCallback or ProvidePasswordCallback when a data source is provided. - - - The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support result columns; use output parameters instead. - - - 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. - - - 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 deleted file mode 100644 index 7f8daed185..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlAggregateMethodCallTranslatorProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlAggregateMethodCallTranslatorProvider : RelationalAggregateMethodCallTranslatorProvider -{ - /// - /// 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 NpgsqlAggregateMethodCallTranslatorProvider( - RelationalAggregateMethodCallTranslatorProviderDependencies dependencies, - IModel model) - : base(dependencies) - { - var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory; - var typeMappingSource = dependencies.RelationalTypeMappingSource; - - AddTranslators( - [ - 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 deleted file mode 100644 index 847da4c171..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ /dev/null @@ -1,210 +0,0 @@ -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; - -/// -/// Translates method and property calls on arrays/lists into their corresponding PostgreSQL operations. -/// -/// -/// https://www.postgresql.org/docs/current/static/functions-array.html -/// -public class NpgsqlArrayMethodTranslator : IMethodCallTranslator -{ - #region Methods - - // ReSharper disable InconsistentNaming - private static readonly MethodInfo Array_IndexOf1 = - typeof(Array).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single(m => m is { Name: nameof(Array.IndexOf), IsGenericMethod: true } && m.GetParameters().Length == 2); - - private static readonly MethodInfo Array_IndexOf2 = - typeof(Array).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single(m => m is { Name: nameof(Array.IndexOf), IsGenericMethod: true } && m.GetParameters().Length == 3); - - private static readonly MethodInfo Enumerable_ElementAt = - typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single( - m => m.Name == nameof(Enumerable.ElementAt) - && m.GetParameters().Length == 2 - && m.GetParameters()[1].ParameterType == typeof(int)); - - 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 - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly NpgsqlJsonPocoTranslator _jsonPocoTranslator; - - /// - /// 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 NpgsqlArrayMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory, NpgsqlJsonPocoTranslator jsonPocoTranslator) - { - _sqlExpressionFactory = sqlExpressionFactory; - _jsonPocoTranslator = jsonPocoTranslator; - } - - /// - /// 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? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; so we handle indexing into array/list here - if (method.IsClosedFormOf(Enumerable_ElementAt)) - { - // Indexing over bytea is special, we have to use function rather than subscript - if (arguments[0].TypeMapping is NpgsqlByteArrayTypeMapping) - { - return _sqlExpressionFactory.Function( - "get_byte", - [arguments[0], arguments[1]], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(byte)); - } - - // Try translating indexing inside JSON column - // Note that Length over PG arrays (not within JSON) gets translated by QueryableMethodTranslatingEV, since arrays are primitive - // collections - return _jsonPocoTranslator.TranslateMemberAccess(arguments[0], arguments[1], method.ReturnType); - } - - if (method.IsClosedFormOf(Enumerable_SequenceEqual) - && arguments[0].Type.IsArrayOrGenericList() - && !IsMappedToNonArray(arguments[0]) - && arguments[1].Type.IsArrayOrGenericList() - && !IsMappedToNonArray(arguments[1])) - { - return _sqlExpressionFactory.Equal(arguments[0], arguments[1]); - } - - // Translate instance methods on List - if (instance is not null && instance.Type.IsGenericList() && !IsMappedToNonArray(instance)) - { - return TranslateCommon(instance, arguments); - } - - // Translate extension methods over array or List - if (instance is null && arguments.Count > 0 && arguments[0].Type.IsArrayOrGenericList() && !IsMappedToNonArray(arguments[0])) - { - return TranslateCommon(arguments[0], arguments.Slice(1)); - } - - return null; - - // The array/list CLR type may be mapped to a non-array database type (e.g. byte[] to bytea, or just - // value converters) - we don't want to translate for those cases. - static bool IsMappedToNonArray(SqlExpression arrayOrList) - => arrayOrList.TypeMapping is { } and not (NpgsqlArrayTypeMapping or NpgsqlJsonTypeMapping); - -#pragma warning disable CS8321 - SqlExpression? TranslateCommon(SqlExpression arrayOrList, IReadOnlyList arguments) -#pragma warning restore CS8321 - { - if (method.IsClosedFormOf(Array_IndexOf1) - || method.Name == nameof(List.IndexOf) - && method.DeclaringType.IsGenericList() - && method.GetParameters().Length == 1) - { - var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); - - return _sqlExpressionFactory.Coalesce( - _sqlExpressionFactory.Subtract( - _sqlExpressionFactory.Function( - "array_position", - [array, item], - nullable: true, - // 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)); - } - - if (method.IsClosedFormOf(Array_IndexOf2) - || method.Name == nameof(List.IndexOf) - && method.DeclaringType.IsGenericList() - && method.GetParameters().Length == 2) - { - var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); - var startIndex = _sqlExpressionFactory.GenerateOneBasedIndexExpression(arguments[1]); - - return _sqlExpressionFactory.Coalesce( - _sqlExpressionFactory.Subtract( - _sqlExpressionFactory.Function( - "array_position", - [array, item, startIndex], - nullable: true, - // 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/NpgsqlBigIntegerMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBigIntegerMemberTranslator.cs deleted file mode 100644 index 6e9a3a5c69..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlBigIntegerMemberTranslator.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Numerics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlBigIntegerMemberTranslator : IMemberTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - private static readonly MemberInfo IsZero = typeof(BigInteger).GetProperty(nameof(BigInteger.IsZero))!; - private static readonly MemberInfo IsOne = typeof(BigInteger).GetProperty(nameof(BigInteger.IsOne))!; - private static readonly MemberInfo IsEven = typeof(BigInteger).GetProperty(nameof(BigInteger.IsEven))!; - - /// - /// 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 NpgsqlBigIntegerMemberTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - if (member.DeclaringType == typeof(BigInteger)) - { - if (member == IsZero) - { - return _sqlExpressionFactory.Equal(instance!, _sqlExpressionFactory.Constant(BigInteger.Zero)); - } - - if (member == IsOne) - { - return _sqlExpressionFactory.Equal(instance!, _sqlExpressionFactory.Constant(BigInteger.One)); - } - - if (member == IsEven) - { - return _sqlExpressionFactory.Equal( - _sqlExpressionFactory.Modulo(instance!, _sqlExpressionFactory.Constant(new BigInteger(2))), - _sqlExpressionFactory.Constant(BigInteger.Zero)); - } - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs deleted file mode 100644 index 4a6a226593..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlByteArrayMethodTranslator.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlByteArrayMethodTranslator : IMethodCallTranslator -{ - 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 NpgsqlByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) - { - _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. - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - Check.NotNull(method, nameof(method)); - Check.NotNull(arguments, nameof(arguments)); - - if (method.IsGenericMethod && arguments[0].TypeMapping is NpgsqlByteArrayTypeMapping typeMapping) - { - // Note: we only translate if the array argument is a column mapped to bytea. There are various other - // cases (e.g. Where(b => new byte[] { 1, 2, 3 }.Contains(b.SomeByte))) where we prefer to translate via - // regular PostgreSQL array logic. - if (method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains)) - { - var source = arguments[0]; - - // We have a byte value, but we need a bytea for PostgreSQL POSITION. - var value = arguments[1] is SqlConstantExpression constantValue - ? _sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, typeMapping) - // Create bytea from non-constant byte: SELECT set_byte('\x00', 0, 8::smallint); - : _sqlExpressionFactory.Function( - "set_byte", - [ - _sqlExpressionFactory.Constant(new[] { (byte)0 }, typeMapping), - _sqlExpressionFactory.Constant(0), - arguments[1] - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - typeof(byte[]), - typeMapping); - - return _sqlExpressionFactory.GreaterThan( - PgFunctionExpression.CreateWithArgumentSeparators( - "position", - [value, source], - ["IN"], // POSITION(x IN y) - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - builtIn: true, - typeof(int), - null), - _sqlExpressionFactory.Constant(0)); - } - - if (method.GetGenericMethodDefinition().Equals(EnumerableMethods.FirstWithoutPredicate)) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Function( - "get_byte", - [arguments[0], _sqlExpressionFactory.Constant(0)], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(byte)), - method.ReturnType); - } - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs deleted file mode 100644 index 84c27eb540..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlConvertTranslator.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Translates methods defined on into PostgreSQL CAST expressions. -/// -public class NpgsqlConvertTranslator : IMethodCallTranslator -{ - private static readonly Dictionary TypeMapping = new() - { - [nameof(Convert.ToBoolean)] = "bool", - [nameof(Convert.ToByte)] = "smallint", - [nameof(Convert.ToDecimal)] = "numeric", - [nameof(Convert.ToDouble)] = "double precision", - [nameof(Convert.ToInt16)] = "smallint", - [nameof(Convert.ToInt32)] = "int", - [nameof(Convert.ToInt64)] = "bigint", - [nameof(Convert.ToString)] = "text" - }; - - private static readonly List SupportedTypes = - [ - typeof(bool), - typeof(byte), - typeof(decimal), - typeof(double), - typeof(float), - typeof(int), - typeof(long), - typeof(short), - typeof(string), - typeof(object) - ]; - - private static readonly List SupportedMethods - = TypeMapping.Keys - .SelectMany( - t => typeof(Convert).GetTypeInfo().GetDeclaredMethods(t) - .Where( - m => m.GetParameters().Length == 1 - && SupportedTypes.Contains(m.GetParameters().First().ParameterType))) - .ToList(); - - 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 NpgsqlConvertTranslator(ISqlExpressionFactory sqlExpressionFactory) - { - _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. - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - => SupportedMethods.Contains(method) - ? _sqlExpressionFactory.Convert(arguments[0], method.ReturnType) - : null; -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs deleted file mode 100644 index bb4e414682..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCubeTranslator.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlCubeTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) : IMethodCallTranslator, IMemberTranslator -{ - private readonly RelationalTypeMapping _cubeTypeMapping = typeMappingSource.FindMapping(typeof(NpgsqlCube))!; - private readonly RelationalTypeMapping _doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!; - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // Handle NpgsqlCubeDbFunctionsExtensions methods - if (method.DeclaringType != typeof(NpgsqlCubeDbFunctionsExtensions)) - { - return null; - } - - return method.Name switch - { - nameof(NpgsqlCubeDbFunctionsExtensions.Overlaps) when arguments is [var cube1, var cube2] - => sqlExpressionFactory.Overlaps(cube1, cube2), - - nameof(NpgsqlCubeDbFunctionsExtensions.Contains) when arguments is [var cube1, var cube2] - => sqlExpressionFactory.Contains(cube1, cube2), - - nameof(NpgsqlCubeDbFunctionsExtensions.ContainedBy) when arguments is [var cube1, var cube2] - => sqlExpressionFactory.ContainedBy(cube1, cube2), - - nameof(NpgsqlCubeDbFunctionsExtensions.Distance) when arguments is [var cube1, var cube2] - => new PgBinaryExpression( - PgExpressionType.Distance, - sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping), - sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping), - typeof(double), - _doubleTypeMapping), - - nameof(NpgsqlCubeDbFunctionsExtensions.DistanceTaxicab) when arguments is [var cube1, var cube2] - => new PgBinaryExpression( - PgExpressionType.CubeDistanceTaxicab, - sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping), - sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping), - typeof(double), - _doubleTypeMapping), - - nameof(NpgsqlCubeDbFunctionsExtensions.DistanceChebyshev) when arguments is [var cube1, var cube2] - => new PgBinaryExpression( - PgExpressionType.CubeDistanceChebyshev, - sqlExpressionFactory.ApplyTypeMapping(cube1, _cubeTypeMapping), - sqlExpressionFactory.ApplyTypeMapping(cube2, _cubeTypeMapping), - typeof(double), - _doubleTypeMapping), - - nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinate) when arguments is [var cube, var index] - => new PgBinaryExpression( - PgExpressionType.CubeNthCoordinate, - sqlExpressionFactory.ApplyTypeMapping(cube, _cubeTypeMapping), - ConvertToPostgresIndex(index), - typeof(double), - _doubleTypeMapping), - - nameof(NpgsqlCubeDbFunctionsExtensions.NthCoordinateKnn) when arguments is [var cube, var index] - => new PgBinaryExpression( - PgExpressionType.CubeNthCoordinateKnn, - sqlExpressionFactory.ApplyTypeMapping(cube, _cubeTypeMapping), - ConvertToPostgresIndex(index), - typeof(double), - _doubleTypeMapping), - - nameof(NpgsqlCubeDbFunctionsExtensions.Union) when arguments is [var cube1, var cube2] - => sqlExpressionFactory.Function( - "cube_union", - [cube1, cube2], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(NpgsqlCube), - typeMappingSource.FindMapping(typeof(NpgsqlCube))), - - nameof(NpgsqlCubeDbFunctionsExtensions.Intersect) when arguments is [var cube1, var cube2] - => sqlExpressionFactory.Function( - "cube_inter", - [cube1, cube2], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(NpgsqlCube), - typeMappingSource.FindMapping(typeof(NpgsqlCube))), - - nameof(NpgsqlCubeDbFunctionsExtensions.Enlarge) when arguments is [var cube1, var cube2, var dimension] - => sqlExpressionFactory.Function( - "cube_enlarge", - [cube1, cube2, dimension], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - typeof(NpgsqlCube), - typeMappingSource.FindMapping(typeof(NpgsqlCube))), - - _ => null - }; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - if (member.DeclaringType != typeof(NpgsqlCube)) - { - return null; - } - - return member.Name switch - { - nameof(NpgsqlCube.Dimensions) - => sqlExpressionFactory.Function( - "cube_dim", - [instance!], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)), - - nameof(NpgsqlCube.IsPoint) - => sqlExpressionFactory.Function( - "cube_is_point", - [instance!], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(bool)), - - nameof(NpgsqlCube.LowerLeft) - => throw new InvalidOperationException( - $"The '{nameof(NpgsqlCube.LowerLeft)}' property cannot be translated to SQL. " + - $"To access individual lower-left coordinates in queries, use indexer syntax (e.g., cube.LowerLeft[index]) instead."), - - nameof(NpgsqlCube.UpperRight) - => throw new InvalidOperationException( - $"The '{nameof(NpgsqlCube.UpperRight)}' property cannot be translated to SQL. " + - $"To access individual upper-right coordinates in queries, use indexer syntax (e.g., cube.UpperRight[index]) instead."), - - _ => null - }; - } - - /// - /// Converts a zero-based index to one-based for PostgreSQL cube functions. - /// For constant indexes, simplifies at translation time to avoid unnecessary addition in SQL. - /// - private SqlExpression ConvertToPostgresIndex(SqlExpression indexExpression) - { - var intTypeMapping = typeMappingSource.FindMapping(typeof(int)); - - return indexExpression is SqlConstantExpression { Value: int index } - ? sqlExpressionFactory.Constant(index + 1, intTypeMapping) - : sqlExpressionFactory.Add(indexExpression, sqlExpressionFactory.Constant(1, intTypeMapping)); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs deleted file mode 100644 index f3a44238fe..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs +++ /dev/null @@ -1,272 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translation services for members. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-datetime.html -/// -public class NpgsqlDateTimeMemberTranslator : IMemberTranslator -{ - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _timestampMapping; - private readonly RelationalTypeMapping _timestampTzMapping; - - /// - /// 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 NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _typeMappingSource = typeMappingSource; - _timestampMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp without time zone")!; - _timestampTzMapping = typeMappingSource.FindMapping(typeof(DateTime), "timestamp with time zone")!; - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - var declaringType = member.DeclaringType; - - if (declaringType != typeof(DateTime) - && declaringType != typeof(DateTimeOffset) - && declaringType != typeof(DateOnly) - && declaringType != typeof(TimeOnly)) - { - return null; - } - - if (declaringType == typeof(DateTimeOffset) - && instance is not null - && TranslateDateTimeOffset(instance, member) is { } translated) - { - 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 - // a PG timestamp/timestamptz) rather than a conversion to PG date (compare with NodaTime where we want a LocalDate). - - // When given a timestamptz, date_trunc performs the truncation with respect to TimeZone; to avoid that, we use the overload - // accepting a time zone, and pass UTC. For regular timestamp (or in legacy timestamp mode), we use the simpler overload without - // a time zone. - switch (instance) - { - case { TypeMapping: NpgsqlTimestampTypeMapping }: - case { } when NpgsqlTypeMappingSource.LegacyTimestampBehavior: - return _sqlExpressionFactory.Function( - "date_trunc", - [_sqlExpressionFactory.Constant("day"), instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - returnType, - instance.TypeMapping); - - case { TypeMapping: NpgsqlTimestampTzTypeMapping }: - return _sqlExpressionFactory.Function( - "date_trunc", - [_sqlExpressionFactory.Constant("day"), instance, _sqlExpressionFactory.Constant("UTC")], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - returnType, - instance.TypeMapping); - - // If DateTime.Date is invoked on a PostgreSQL date (or DateOnly, which can only be mapped to datE), simply no-op. - case { TypeMapping: NpgsqlDateTimeDateTypeMapping }: - case { Type: var type } when type == typeof(DateOnly): - return instance; - - default: - return null; - } - } - - return member.Name switch - { - // Legacy behavior - nameof(DateTime.Now) when NpgsqlTypeMappingSource.LegacyTimestampBehavior - => UtcNow(), - nameof(DateTime.UtcNow) when NpgsqlTypeMappingSource.LegacyTimestampBehavior - => _sqlExpressionFactory.AtUtc(UtcNow()), // Return a UTC timestamp, but as timestamp without time zone - - // We support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a non-UTC - // DateTimeOffset. - nameof(DateTime.Now) => declaringType == typeof(DateTimeOffset) - ? throw new InvalidOperationException("Cannot translate DateTimeOffset.Now - use UtcNow.") - : LocalNow(), - nameof(DateTime.UtcNow) => UtcNow(), - - nameof(DateTime.Today) => _sqlExpressionFactory.Function( - "date_trunc", - [_sqlExpressionFactory.Constant("day"), LocalNow()], - nullable: false, - argumentsPropagateNullability: FalseArrays[2], - typeof(DateTime), - _timestampMapping), - - nameof(DateTime.Year) => DatePart(instance!, "year"), - nameof(DateTime.Month) => DatePart(instance!, "month"), - nameof(DateTime.DayOfYear) => DatePart(instance!, "doy"), - nameof(DateTime.Day) => DatePart(instance!, "day"), - nameof(DateTime.Hour) => DatePart(instance!, "hour"), - nameof(DateTime.Minute) => DatePart(instance!, "minute"), - nameof(DateTime.Second) => DatePart(instance!, "second"), - - nameof(DateTime.Millisecond) => null, // Too annoying - - // .NET's DayOfWeek is an enum, but its int values happen to correspond to PostgreSQL - nameof(DateTime.DayOfWeek) => DatePart(instance!, "dow", floor: true), - - // Casting a timestamptz to time (to get the time component) converts it to a local timestamp based on TimeZone. - // Convert to a timestamp without time zone at UTC to get the right values. - nameof(DateTime.TimeOfDay) when TryConvertAwayFromTimestampTz(instance!, out var convertedInstance) - => _sqlExpressionFactory.Convert( - convertedInstance, - typeof(TimeSpan), - _typeMappingSource.FindMapping(typeof(TimeSpan), storeTypeName: "time")), - - // TODO: Should be possible - nameof(DateTime.Ticks) => null, - - _ => null - }; - - SqlExpression UtcNow() - => _sqlExpressionFactory.Function( - "now", - [], - nullable: false, - argumentsPropagateNullability: TrueArrays[0], - returnType, - _timestampTzMapping); - - SqlExpression LocalNow() - => _sqlExpressionFactory.Convert(UtcNow(), returnType, _timestampMapping); - } - - private SqlExpression? DatePart( - SqlExpression instance, - string partName, - bool floor = false) - { - // date_part exists only for timestamp without time zone, so if we pass in a timestamptz it gets converted to a local - // timestamp based on TimeZone. Convert to a timestamp without time zone at UTC to get the right values. - if (!TryConvertAwayFromTimestampTz(instance, out instance!)) - { - return null; - } - - var result = _sqlExpressionFactory.Function( - "date_part", - [_sqlExpressionFactory.Constant(partName), instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(double)); - - if (floor) - { - result = _sqlExpressionFactory.Function( - "floor", - [result], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(double)); - } - - return _sqlExpressionFactory.Convert(result, typeof(int)); - } - - 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 - nameof(DateTimeOffset.DateTime) => _sqlExpressionFactory.AtUtc(instance), - - // We only support UTC DateTimeOffset, so DateTimeOffset.UtcDateTime does nothing (type change on CLR change, no change on the - // PG side. - nameof(DateTimeOffset.UtcDateTime) => instance, - - // Convert to timestamp without time zone, applying a time zone conversion based on the TimeZone connection parameter. - nameof(DateTimeOffset.LocalDateTime) => _sqlExpressionFactory.Convert(instance, typeof(DateTime), _timestampMapping), - - // In PG, date_trunc over timestamptz looks at TimeZone, and returns timestamptz. .NET DateTimeOffset.Date just returns the - // date part (no conversion), and returns an Unspecified DateTime. So we first convert the timestamptz argument to timestamp - // via AT TIME ZONE 'UTC". - // Note that we don't use the overload of date_trunc that accepts a timezone as its 3rd argument (like we do for DateTime.Date), - // since that returns a timestamptz, but DateTimeOffset.Date should return DateTime with Kind=Unspecified - nameof(DateTimeOffset.Date) => - _sqlExpressionFactory.Function( - "date_trunc", - [_sqlExpressionFactory.Constant("day"), _sqlExpressionFactory.AtUtc(instance)], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(DateTime), - _timestampMapping), - - _ => 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). - // To avoid this, if we get a timestamptz, convert it to a timestamp without time zone (at UTC), which doesn't undergo any timezone - // conversions. - private bool TryConvertAwayFromTimestampTz(SqlExpression timestamp, [NotNullWhen(true)] out SqlExpression? result) - { - switch (timestamp) - { - // We're already dealing with a non-timestamptz mapping, no conversion needed. - case { TypeMapping: NpgsqlTimestampTypeMapping or NpgsqlDateTimeDateTypeMapping or NpgsqlTimeTypeMapping }: - case { Type: var type } when type == typeof(DateOnly) || type == typeof(TimeOnly): - result = timestamp; - return true; - - // In these cases we know that the expression represents a timestamptz; it's safe to convert to a timestamp without time zone. - // Note that timestamptz AT TIME ZONE 'UTC' returns the same timestamp but as a timestamp (without time zone). - case { TypeMapping: NpgsqlTimestampTzTypeMapping }: - case { Type: var type } when type == typeof(DateTimeOffset): - result = _sqlExpressionFactory.AtUtc(timestamp); - return true; - - // If it's a DateTime who's type mapping isn't known (parameter), we cannot ensure that a timestamp without time zone - // is returned (note that applying AT TIME ZONE 'UTC' on a timestamp without time zone would yield a timestamptz, which would - // again undergo timestamp conversion) - case { Type: var type } when type == typeof(DateTime): - result = null; - return false; - - default: - throw new UnreachableException(); - } - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs deleted file mode 100644 index 2cb2a5d223..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMethodTranslator.cs +++ /dev/null @@ -1,401 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlDateTimeMethodTranslator : IMethodCallTranslator -{ - private static readonly Dictionary MethodInfoDatePartMapping = new() - { - { 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), [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), [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), [])!; - - private static readonly MethodInfo DateTime_ToLocalTime - = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToLocalTime), [])!; - - private static readonly MethodInfo DateTime_SpecifyKind - = typeof(DateTime).GetRuntimeMethod(nameof(DateTime.SpecifyKind), [typeof(DateTime), typeof(DateTimeKind)])!; - - private static readonly MethodInfo DateTime_Distance - = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(DateTime), typeof(DateTime)])!; - - private static readonly MethodInfo DateOnly_FromDateTime - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.FromDateTime), [typeof(DateTime)])!; - - private static readonly MethodInfo DateOnly_ToDateTime - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.ToDateTime), [typeof(TimeOnly)])!; - - private static readonly MethodInfo DateOnly_Distance - = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.Distance), [typeof(DbFunctions), typeof(DateOnly), typeof(DateOnly)])!; - - private static readonly MethodInfo DateOnly_AddDays - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), [typeof(int)])!; - - private static readonly MethodInfo DateOnly_AddMonths - = typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), [typeof(int)])!; - - private static readonly MethodInfo DateOnly_AddYears - = 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), [typeof(DateTime)])!; - - private static readonly MethodInfo TimeOnly_FromTimeSpan - = 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), [typeof(TimeOnly), typeof(TimeOnly)])!; - - private static readonly MethodInfo TimeOnly_Add_TimeSpan - = typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), [typeof(TimeSpan)])!; - - private static readonly MethodInfo TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTime - = typeof(TimeZoneInfo).GetRuntimeMethod( - nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [typeof(DateTime), typeof(string)])!; - - private static readonly MethodInfo TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTimeOffset - = typeof(TimeZoneInfo).GetRuntimeMethod( - nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), [typeof(DateTimeOffset), typeof(string)])!; - - private static readonly MethodInfo TimeZoneInfo_ConvertTimeToUtc - = typeof(TimeZoneInfo).GetRuntimeMethod(nameof(TimeZoneInfo.ConvertTimeToUtc), [typeof(DateTime)])!; - // ReSharper restore InconsistentNaming - - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _timestampMapping; - private readonly RelationalTypeMapping _timestampTzMapping; - private readonly RelationalTypeMapping _intervalMapping; - private readonly RelationalTypeMapping _textMapping; - - /// - /// 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 NpgsqlDateTimeMethodTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - _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")!; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - => TranslateDateTime(instance, method, arguments) - ?? TranslateDateOnly(instance, method, arguments) - ?? TranslateTimeOnly(instance, method, arguments) - ?? TranslateTimeZoneInfo(method, arguments) - ?? TranslateDatePart(instance, method, arguments); - - private SqlExpression? TranslateDatePart( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - => instance is not null - && MethodInfoDatePartMapping.TryGetValue(method, out var datePart) - && CreateIntervalExpression(arguments[0], datePart) is SqlExpression interval - ? _sqlExpressionFactory.Add(instance, interval, instance.TypeMapping) - : null; - - private SqlExpression? TranslateDateTime( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (instance is null) - { - if (method == DateTime_SpecifyKind) - { - if (arguments[1] is not SqlConstantExpression { Value: DateTimeKind kind }) - { - throw new InvalidOperationException("Translating SpecifyKind is only supported with a constant Kind argument"); - } - - var typeMapping = arguments[0].TypeMapping; - - if (typeMapping is not NpgsqlTimestampTypeMapping and not NpgsqlTimestampTzTypeMapping) - { - throw new InvalidOperationException("Translating SpecifyKind is only supported on timestamp/timestamptz columns"); - } - - if (kind == DateTimeKind.Utc) - { - return typeMapping is NpgsqlTimestampTypeMapping - ? _sqlExpressionFactory.AtUtc(arguments[0]) - : arguments[0]; - } - - if (kind is DateTimeKind.Unspecified or DateTimeKind.Local) - { - return typeMapping is NpgsqlTimestampTzTypeMapping - ? _sqlExpressionFactory.AtUtc(arguments[0]) - : arguments[0]; - } - } - - if (method == DateTime_Distance) - { - return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.Distance, arguments[1], arguments[2]); - } - } - else - { - if (method == DateTime_ToUniversalTime) - { - return _sqlExpressionFactory.Convert(instance, method.ReturnType, _timestampTzMapping); - } - - if (method == DateTime_ToLocalTime) - { - return _sqlExpressionFactory.Convert(instance, method.ReturnType, _timestampMapping); - } - } - - return null; - } - - private SqlExpression? TranslateDateOnly( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (instance is null) - { - if (method == DateOnly_FromDateTime) - { - // Note: converting timestamptz to date performs a timezone conversion, which is not what .NET DateOnly.FromDateTime does. - // So if our operand is a timestamptz, we first change the type to timestamp with AT TIME ZONE 'UTC' (returns the same value - // but as a timestamptz). - // If our operand is already timestamp, no need to do anything. We throw for anything else to avoid accidentally applying - // AT TIME ZONE to a non-timestamptz, which would do a timezone conversion - var dateTime = arguments[0].TypeMapping switch - { - NpgsqlTimestampTypeMapping => arguments[0], - NpgsqlTimestampTzTypeMapping => _sqlExpressionFactory.AtUtc(arguments[0]), - _ => throw new NotSupportedException("Can only apply TimeOnly.FromDateTime on a timestamp or timestamptz column") - }; - - return _sqlExpressionFactory.Convert(dateTime, typeof(DateOnly), _typeMappingSource.FindMapping(typeof(DateOnly))); - } - - if (method == DateOnly_Distance) - { - 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 - { - if (method == DateOnly_ToDateTime) - { - return new SqlBinaryExpression( - ExpressionType.Add, - _sqlExpressionFactory.ApplyDefaultTypeMapping(instance), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), - typeof(DateTime), - _timestampMapping); - } - - // In PG, date + int = date (int interpreted as days) - if (method == DateOnly_AddDays) - { - return _sqlExpressionFactory.Add(instance, arguments[0]); - } - - // For months and years, date + interval yields a timestamp (since interval could have a time component), so we need to cast - // the results back to date - if (method == DateOnly_AddMonths - && CreateIntervalExpression(arguments[0], "months") is SqlExpression interval1) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Add(instance, interval1, instance.TypeMapping), typeof(DateOnly)); - } - - if (method == DateOnly_AddYears - && CreateIntervalExpression(arguments[0], "years") is SqlExpression interval2) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Add(instance, interval2, instance.TypeMapping), typeof(DateOnly)); - } - } - - return null; - } - - private SqlExpression? TranslateTimeOnly( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments) - { - if (method == TimeOnly_FromDateTime) - { - // Note: converting timestamptz to time performs a timezone conversion, which is not what .NET TimeOnly.FromDateTime does. - // So if our operand is a timestamptz, we first change the type to timestamp with AT TIME ZONE 'UTC' (returns the same value - // but as a timestamptz). - // If our operand is already timestamp, no need to do anything. We throw for anything else to avoid accidentally applying - // AT TIME ZONE to a non-timestamptz, which would do a timezone conversion - var dateTime = arguments[0].TypeMapping switch - { - NpgsqlTimestampTypeMapping => arguments[0], - NpgsqlTimestampTzTypeMapping => _sqlExpressionFactory.AtUtc(arguments[0]), - _ => throw new NotSupportedException("Can only apply TimeOnly.FromDateTime on a timestamp or timestamptz column") - }; - - return _sqlExpressionFactory.Convert( - dateTime, - typeof(TimeOnly), - _typeMappingSource.FindMapping(typeof(TimeOnly))); - } - - if (method == TimeOnly_FromTimeSpan) - { - return _sqlExpressionFactory.Convert(arguments[0], typeof(TimeOnly), _typeMappingSource.FindMapping(typeof(TimeOnly))); - } - - if (instance is not null) - { - if (method == TimeOnly_ToTimeSpan) - { - return _sqlExpressionFactory.Convert(instance, typeof(TimeSpan), _typeMappingSource.FindMapping(typeof(TimeSpan))); - } - - if (method == TimeOnly_IsBetween) - { - return _sqlExpressionFactory.And( - _sqlExpressionFactory.GreaterThanOrEqual(instance, arguments[0]), - _sqlExpressionFactory.LessThan(instance, arguments[1])); - } - - if (method == TimeOnly_Add_TimeSpan) - { - return _sqlExpressionFactory.Add(instance, arguments[0]); - } - } - - return null; - } - - private SqlExpression? TranslateTimeZoneInfo( - MethodInfo method, - IReadOnlyList arguments) - { - if (method == TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTime) - { - var typeMapping = arguments[0].TypeMapping; - if (typeMapping is null - || (typeMapping.StoreType != "timestamp with time zone" && typeMapping.StoreType != "timestamptz")) - { - throw new InvalidOperationException( - "TimeZoneInfo.ConvertTimeBySystemTimeZoneId is only supported on columns with type 'timestamp with time zone'"); - } - - return _sqlExpressionFactory.AtTimeZone(arguments[0], arguments[1], typeof(DateTime), _timestampMapping); - } - - if (method == TimeZoneInfo_ConvertTimeToUtc) - { - var typeMapping = arguments[0].TypeMapping; - if (typeMapping is null - || (typeMapping.StoreType != "timestamp without time zone" && typeMapping.StoreType != "timestamp")) - { - throw new InvalidOperationException( - "TimeZoneInfo.ConvertTimeToUtc) is only supported on columns with type 'timestamp without time zone'"); - } - - return _sqlExpressionFactory.Convert(arguments[0], arguments[0].Type, _timestampTzMapping); - } - - return null; - } - - private SqlExpression? CreateIntervalExpression(SqlExpression intervalNum, string datePart) - { - // Note: ideally we'd simply generate a PostgreSQL interval expression, but the .NET mapping of that is TimeSpan, - // which does not work for months, years, etc. So we generate special fragments instead. - if (intervalNum is SqlConstantExpression constantExpression) - { - // We generate constant intervals as INTERVAL '1 days' - if (constantExpression.Type == typeof(double) - && ((double)constantExpression.Value! >= int.MaxValue || (double)constantExpression.Value <= int.MinValue)) - { - return null; - } - - return _sqlExpressionFactory.Fragment(FormattableString.Invariant($"INTERVAL '{constantExpression.Value} {datePart}'")); - } - - // For non-constants, we can't parameterize INTERVAL '1 days'. Instead, we use CAST($1 || ' days' AS interval). - // Note that a make_interval() function also exists, but accepts only int (for all fields except for - // seconds), so we don't use it. - // Note: we instantiate SqlBinaryExpression manually rather than via sqlExpressionFactory because - // of the non-standard Add expression (concatenate int with text) - return _sqlExpressionFactory.Convert( - new SqlBinaryExpression( - ExpressionType.Add, - _sqlExpressionFactory.Convert(intervalNum, typeof(string), _textMapping), - _sqlExpressionFactory.Constant(' ' + datePart, _textMapping), - typeof(string), - _textMapping), - typeof(TimeSpan), - _intervalMapping); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs deleted file mode 100644 index e4db8fba87..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs +++ /dev/null @@ -1,350 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translations for PostgreSQL full-text search methods. -/// -public class NpgsqlFullTextSearchMethodTranslator : IMethodCallTranslator -{ - private static readonly MethodInfo TsQueryParse = - typeof(NpgsqlTsQuery).GetMethod(nameof(NpgsqlTsQuery.Parse), BindingFlags.Public | BindingFlags.Static)!; - - private static readonly MethodInfo TsVectorParse = - typeof(NpgsqlTsVector).GetMethod(nameof(NpgsqlTsVector.Parse), BindingFlags.Public | BindingFlags.Static)!; - - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IModel _model; - private readonly RelationalTypeMapping _tsQueryMapping; - private readonly RelationalTypeMapping _tsVectorMapping; - private readonly RelationalTypeMapping _regconfigMapping; - private readonly RelationalTypeMapping _regdictionaryMapping; - - /// - /// 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 NpgsqlFullTextSearchMethodTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - _model = model; - _tsQueryMapping = typeMappingSource.FindMapping("tsquery")!; - _tsVectorMapping = typeMappingSource.FindMapping("tsvector")!; - _regconfigMapping = typeMappingSource.FindMapping("regconfig")!; - _regdictionaryMapping = typeMappingSource.FindMapping("regdictionary")!; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method == TsQueryParse || method == TsVectorParse) - { - return _sqlExpressionFactory.Convert(arguments[0], method.ReturnType); - } - - if (method.DeclaringType == typeof(NpgsqlFullTextSearchDbFunctionsExtensions)) - { - return method.Name switch - { - // Methods accepting a configuration (regconfig) - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsVector) when arguments.Count == 3 - => ConfigAccepting("to_tsvector"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PlainToTsQuery) when arguments.Count == 3 - => ConfigAccepting("plainto_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PhraseToTsQuery) when arguments.Count == 3 - => ConfigAccepting("phraseto_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsQuery) when arguments.Count == 3 - => ConfigAccepting("to_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.WebSearchToTsQuery) when arguments.Count == 3 - => ConfigAccepting("websearch_to_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.Unaccent) when arguments.Count == 3 - => DictionaryAccepting("unaccent"), - - // Methods not accepting a configuration - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ArrayToTsVector) - => NonConfigAccepting("array_to_tsvector"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsVector) - => NonConfigAccepting("to_tsvector"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PlainToTsQuery) - => NonConfigAccepting("plainto_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PhraseToTsQuery) - => NonConfigAccepting("phraseto_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsQuery) - => NonConfigAccepting("to_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.WebSearchToTsQuery) - => NonConfigAccepting("websearch_to_tsquery"), - nameof(NpgsqlFullTextSearchDbFunctionsExtensions.Unaccent) - => NonConfigAccepting("unaccent"), - - _ => null - }; - } - - if (method.DeclaringType == typeof(NpgsqlFullTextSearchLinqExtensions)) - { - if (method.Name is - nameof(NpgsqlFullTextSearchLinqExtensions.Rank) or nameof(NpgsqlFullTextSearchLinqExtensions.RankCoverDensity)) - { - var rankFunctionName = method.Name == nameof(NpgsqlFullTextSearchLinqExtensions.Rank) ? "ts_rank" : "ts_rank_cd"; - - return arguments.Count switch - { - 2 => _sqlExpressionFactory.Function( - rankFunctionName, - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(float), - _typeMappingSource.FindMapping(typeof(float), _model)), - - 3 => _sqlExpressionFactory.Function( - rankFunctionName, - [ - 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), - _typeMappingSource.FindMapping(typeof(float), _model)), - - 4 => _sqlExpressionFactory.Function( - rankFunctionName, - [arguments[1], arguments[0], arguments[2], arguments[3]], - nullable: true, - argumentsPropagateNullability: TrueArrays[4], - method.ReturnType, - _typeMappingSource.FindMapping(typeof(float), _model)), - - _ => throw new ArgumentException($"Invalid method overload for {rankFunctionName}") - }; - } - - if (method.Name == nameof(NpgsqlFullTextSearchLinqExtensions.SetWeight)) - { - var newArgs = new List(arguments); - if (newArgs[1].Type == typeof(NpgsqlTsVector.Lexeme.Weight)) - { - newArgs[1] = newArgs[1] is SqlConstantExpression weightExpression - ? _sqlExpressionFactory.Constant(weightExpression.Value!.ToString()![0]) - : throw new ArgumentException("Enum 'weight' argument for 'SetWeight' must be a constant expression."); - } - - return _sqlExpressionFactory.Function( - "setweight", - newArgs, - nullable: true, - argumentsPropagateNullability: TrueArrays[newArgs.Count], - method.ReturnType); - } - - return method.Name switch - { - // Operators - - nameof(NpgsqlFullTextSearchLinqExtensions.And) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.TextSearchAnd, arguments[0], arguments[1]), - nameof(NpgsqlFullTextSearchLinqExtensions.Or) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.TextSearchOr, arguments[0], arguments[1]), - - nameof(NpgsqlFullTextSearchLinqExtensions.ToNegative) - => new SqlUnaryExpression( - ExpressionType.Not, arguments[0], arguments[0].Type, - arguments[0].TypeMapping), - - nameof(NpgsqlFullTextSearchLinqExtensions.Contains) - => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), - nameof(NpgsqlFullTextSearchLinqExtensions.IsContainedIn) - => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), - - nameof(NpgsqlFullTextSearchLinqExtensions.Concat) - => _sqlExpressionFactory.Add(arguments[0], arguments[1], _tsVectorMapping), - - nameof(NpgsqlFullTextSearchLinqExtensions.Matches) - => _sqlExpressionFactory.MakePostgresBinary( - PgExpressionType.TextSearchMatch, - arguments[0], - arguments[1].Type == typeof(string) - ? _sqlExpressionFactory.Function( - "plainto_tsquery", - [arguments[1]], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(NpgsqlTsQuery), - _tsQueryMapping) - : arguments[1]), - - // Functions - - nameof(NpgsqlFullTextSearchLinqExtensions.GetNodeCount) - => _sqlExpressionFactory.Function( - "numnode", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int), - _typeMappingSource.FindMapping(method.ReturnType, _model)), - - nameof(NpgsqlFullTextSearchLinqExtensions.GetQueryTree) - => _sqlExpressionFactory.Function( - "querytree", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(string), - _typeMappingSource.FindMapping(method.ReturnType, _model)), - - nameof(NpgsqlFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 2 - => _sqlExpressionFactory.Function( - "ts_headline", - [arguments[1], arguments[0]], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType), - - nameof(NpgsqlFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 3 => - _sqlExpressionFactory.Function( - "ts_headline", - [arguments[1], arguments[0], arguments[2]], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - method.ReturnType), - - nameof(NpgsqlFullTextSearchLinqExtensions.GetResultHeadline) when arguments.Count == 4 => - _sqlExpressionFactory.Function( - "ts_headline", - [ - // 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. - arguments[1] is SqlConstantExpression constant - ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) - : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regconfigMapping), - arguments[2], - arguments[0], - arguments[3] - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[4], - method.ReturnType), - - nameof(NpgsqlFullTextSearchLinqExtensions.Rewrite) when arguments.Count == 2 - => _sqlExpressionFactory.Function( - "ts_rewrite", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(NpgsqlTsQuery), - _tsQueryMapping), - - nameof(NpgsqlFullTextSearchLinqExtensions.Rewrite) when arguments.Count == 3 - => _sqlExpressionFactory.Function( - "ts_rewrite", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - typeof(NpgsqlTsQuery), - _tsQueryMapping), - - nameof(NpgsqlFullTextSearchLinqExtensions.ToPhrase) - => _sqlExpressionFactory.Function( - "tsquery_phrase", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count], - typeof(NpgsqlTsQuery), - _tsQueryMapping), - - nameof(NpgsqlFullTextSearchLinqExtensions.Delete) - => _sqlExpressionFactory.Function( - "ts_delete", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - _tsVectorMapping), - - // TODO: Here we need to cast the char[] array we got into a "char"[] internal array... - nameof(NpgsqlFullTextSearchLinqExtensions.Filter) - => throw new NotImplementedException(), - //=> _sqlExpressionFactory.Function("ts_filter", arguments, typeof(NpgsqlTsVector), _tsVectorMapping), - - nameof(NpgsqlFullTextSearchLinqExtensions.GetLength) - => _sqlExpressionFactory.Function( - "length", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - method.ReturnType, - _typeMappingSource.FindMapping(typeof(int), _model)), - - nameof(NpgsqlFullTextSearchLinqExtensions.ToStripped) - => _sqlExpressionFactory.Function( - "strip", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count], - method.ReturnType, - _tsVectorMapping), - - _ => null - }; - } - - return null; - - SqlExpression ConfigAccepting(string functionName) - => _sqlExpressionFactory.Function( - 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. - arguments[1] is SqlConstantExpression constant - ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) - : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regconfigMapping), - arguments[2] - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, _model)); - - SqlExpression DictionaryAccepting(string functionName) - => _sqlExpressionFactory.Function( - 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. - arguments[1] is SqlConstantExpression constant - ? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant) - : _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regdictionaryMapping), - arguments[2] - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, _model)); - - SqlExpression NonConfigAccepting(string functionName) - => _sqlExpressionFactory.Function( - functionName, - [arguments[1]], - nullable: true, - 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 deleted file mode 100644 index b0c6aa6516..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlFuzzyStringMatchMethodTranslator : IMethodCallTranslator -{ - private static readonly Dictionary Functions = new() - { - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchSoundex), typeof(DbFunctions), typeof(string))] - = "soundex", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDifference), typeof(DbFunctions), typeof(string), typeof(string))] - = "difference", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein), typeof(DbFunctions), typeof(string), typeof(string))] - = "levenshtein", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein), typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(int), typeof(int))] - = "levenshtein", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshteinLessEqual), typeof(DbFunctions), typeof(string), typeof(string), typeof(int))] - = "levenshtein_less_equal", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshteinLessEqual), typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(int), typeof(int), typeof(int))] - = "levenshtein_less_equal", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchMetaphone), typeof(DbFunctions), typeof(string), typeof(int))] - = "metaphone", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDoubleMetaphone), typeof(DbFunctions), typeof(string))] - = "dmetaphone", - [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDoubleMetaphoneAlt), typeof(DbFunctions), typeof(string))] - = "dmetaphone_alt" - }; - - private static MethodInfo GetRuntimeMethod(string name, params Type[] parameters) - => typeof(NpgsqlFuzzyStringMatchDbFunctionsExtensions).GetRuntimeMethod(name, parameters)!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - private static readonly bool[][] TrueArrays = - [ - [], - [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 - /// 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 NpgsqlFuzzyStringMatchMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - => Functions.TryGetValue(method, out var function) - ? _sqlExpressionFactory.Function( - function, - arguments.Skip(1), - nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count - 1], - method.ReturnType) - : null; -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlGuidTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlGuidTranslator.cs deleted file mode 100644 index f4e41f89fc..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlGuidTranslator.cs +++ /dev/null @@ -1,57 +0,0 @@ -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translation services for PostgreSQL UUID functions. -/// -/// -/// See: https://www.postgresql.org/docs/current/datatype-uuid.html -/// -public class NpgsqlGuidTranslator(ISqlExpressionFactory sqlExpressionFactory, Version? postgresVersion) : IMethodCallTranslator -{ - private readonly string _uuidGenerationFunction = postgresVersion.AtLeast(13) ? "gen_random_uuid" : "uuid_generate_v4"; - - /// - /// 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? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType == typeof(Guid)) - { - return method.Name switch - { - nameof(Guid.NewGuid) - => sqlExpressionFactory.Function( - _uuidGenerationFunction, - [], - nullable: false, - argumentsPropagateNullability: FalseArrays[0], - method.ReturnType), - - // Note: uuidv7() was introduce in PostgreSQL 18. - // In NpgsqlEvaluatableExpressionFilter we only prevent local evaluation when targeting PG18 or later; - // that means that for lower version, the call gets evaluated locally and the result sent as a parameter - // (and we never see the method call here). - nameof(Guid.CreateVersion7) - => sqlExpressionFactory.Function( - "uuidv7", - [], - nullable: false, - argumentsPropagateNullability: FalseArrays[0], - method.ReturnType), - - _ => null - }; - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs deleted file mode 100644 index 122b3e0c50..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDbFunctionsTranslator.cs +++ /dev/null @@ -1,128 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlJsonDbFunctionsTranslator : IMethodCallTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _stringTypeMapping; - private readonly RelationalTypeMapping _jsonbTypeMapping; - - /// - /// 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 NpgsqlJsonDbFunctionsTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _sqlExpressionFactory = sqlExpressionFactory; - _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), model)!; - _jsonbTypeMapping = typeMappingSource.FindMapping("jsonb")!; - } - - /// - /// 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? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType != typeof(NpgsqlJsonDbFunctionsExtensions)) - { - return null; - } - - var args = arguments - // Skip useless DbFunctions instance - .Skip(1) - // JSON extensions accept object parameters for JSON, since they must be able to handle POCOs, strings or DOM types. - // This means they come wrapped in a convert node, which we need to remove. - // Convert nodes may also come from wrapping JsonTraversalExpressions generated through POCO traversal. - .Select(RemoveConvert) - // If a function is invoked over a JSON traversal expression, that expression may come with - // returnText: true (i.e. operator ->> and not ->). Since the functions below require a json object and - // not text, we transform it. - .Select(a => a is PgJsonTraversalExpression traversal ? WithReturnsText(traversal, false) : a) - .ToArray(); - - if (!args.Any(a => a.TypeMapping is NpgsqlJsonTypeMapping || a is PgJsonTraversalExpression)) - { - throw new InvalidOperationException("The EF JSON methods require a JSON parameter and none was found."); - } - - if (method.Name == nameof(NpgsqlJsonDbFunctionsExtensions.JsonTypeof)) - { - return _sqlExpressionFactory.Function( - ((NpgsqlJsonTypeMapping)args[0].TypeMapping!).IsJsonb ? "jsonb_typeof" : "json_typeof", - [args[0]], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(string)); - } - - // The following are jsonb-only, not support on json - if (args.Any(a => a.TypeMapping is NpgsqlJsonTypeMapping { IsJsonb: false })) - { - throw new InvalidOperationException("JSON methods on EF.Functions only support the jsonb type, not json."); - } - - return method.Name switch - { - nameof(NpgsqlJsonDbFunctionsExtensions.JsonContains) - => _sqlExpressionFactory.Contains(Jsonb(args[0]), Jsonb(args[1])), - nameof(NpgsqlJsonDbFunctionsExtensions.JsonContained) - => _sqlExpressionFactory.ContainedBy(Jsonb(args[0]), Jsonb(args[1])), - nameof(NpgsqlJsonDbFunctionsExtensions.JsonExists) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.JsonExists, Jsonb(args[0]), args[1]), - nameof(NpgsqlJsonDbFunctionsExtensions.JsonExistAny) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.JsonExistsAny, Jsonb(args[0]), args[1]), - nameof(NpgsqlJsonDbFunctionsExtensions.JsonExistAll) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.JsonExistsAll, Jsonb(args[0]), args[1]), - - _ => null - }; - - SqlExpression Jsonb(SqlExpression e) - => e.TypeMapping?.StoreType == "jsonb" - ? e - : e is SqlConstantExpression or SqlParameterExpression - ? _sqlExpressionFactory.ApplyTypeMapping(e, _jsonbTypeMapping) - : _sqlExpressionFactory.Convert(e, typeof(string), _jsonbTypeMapping); - - static SqlExpression RemoveConvert(SqlExpression e) - { - while (e is SqlUnaryExpression { OperatorType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) - { - e = unary.Operand; - } - - return e; - } - - PgJsonTraversalExpression WithReturnsText(PgJsonTraversalExpression traversal, bool returnsText) - => traversal.ReturnsText == returnsText - ? traversal - : returnsText - ? new PgJsonTraversalExpression(traversal.Expression, traversal.Path, true, typeof(string), _stringTypeMapping) - : new PgJsonTraversalExpression( - traversal.Expression, traversal.Path, false, traversal.Type, traversal.Expression.TypeMapping); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs deleted file mode 100644 index ec838d19d2..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonDomTranslator.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Text.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 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), [typeof(string)])!; - - private static readonly MethodInfo GetArrayLength = typeof(JsonElement).GetRuntimeMethod( - nameof(JsonElement.GetArrayLength), Type.EmptyTypes)!; - - private static readonly MethodInfo ArrayIndexer = typeof(JsonElement).GetProperties() - .Single(p => p.GetIndexParameters().Length == 1 && p.GetIndexParameters()[0].ParameterType == typeof(int)) - .GetMethod!; - - private static readonly string[] GetMethods = - [ - nameof(JsonElement.GetBoolean), - nameof(JsonElement.GetDateTime), - nameof(JsonElement.GetDateTimeOffset), - nameof(JsonElement.GetDecimal), - nameof(JsonElement.GetDouble), - nameof(JsonElement.GetGuid), - nameof(JsonElement.GetInt16), - nameof(JsonElement.GetInt32), - nameof(JsonElement.GetInt64), - nameof(JsonElement.GetSingle), - nameof(JsonElement.GetString) - ]; - - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _stringTypeMapping; - private readonly 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 - /// 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 NpgsqlJsonDomTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - _model = model; - _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), 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 SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - if (member.DeclaringType != typeof(JsonDocument)) - { - return null; - } - - if (member == RootElement && instance is ColumnExpression { TypeMapping: NpgsqlJsonTypeMapping } column) - { - // Simply get rid of the RootElement member access - return column; - } - - return 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 virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType != typeof(JsonElement) || instance?.TypeMapping is not NpgsqlJsonTypeMapping mapping) - { - return null; - } - - // The root of the JSON expression is a ColumnExpression. We wrap that with an empty traversal - // expression (col #>> '{}'); subsequent traversals will gradually append the path into that. - // Note that it's possible to call methods such as GetString() directly on the root, and the - // empty traversal is necessary to properly convert it to a text. - instance = instance is ColumnExpression columnExpression - ? _sqlExpressionFactory.JsonTraversal( - columnExpression, returnsText: false, typeof(string), mapping) - : instance; - - if (method == GetProperty || method == ArrayIndexer) - { - return instance is PgJsonTraversalExpression prevPathTraversal - ? prevPathTraversal.Append(_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0])) - : null; - } - - if (GetMethods.Contains(method.Name) && arguments.Count == 0 && instance is PgJsonTraversalExpression traversal) - { - var traversalToText = new PgJsonTraversalExpression( - traversal.Expression, - traversal.Path, - returnsText: true, - 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 - : _sqlExpressionFactory.Convert( - traversalToText, method.ReturnType, _typeMappingSource.FindMapping(method.ReturnType, _model)); - } - - if (method == GetArrayLength) - { - return _sqlExpressionFactory.Function( - mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)); - } - - if (method.Name.StartsWith("TryGet", StringComparison.Ordinal) && arguments.Count == 0) - { - throw new InvalidOperationException($"The TryGet* methods on {nameof(JsonElement)} aren't translated yet, use Get* instead.'"); - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs deleted file mode 100644 index fbaf19142e..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlJsonPocoTranslator.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Text.Json.Serialization; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlJsonPocoTranslator : IMemberTranslator, IMethodCallTranslator -{ - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _stringTypeMapping; - private readonly IModel _model; - - private static readonly MethodInfo Enumerable_AnyWithoutPredicate = - typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1); - - /// - /// 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 NpgsqlJsonPocoTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - _model = model; - _stringTypeMapping = typeMappingSource.FindMapping(typeof(string), 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 SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // Predicate-less Any - translate to a simple length check. - if (method.IsClosedFormOf(Enumerable_AnyWithoutPredicate) - && TranslateArrayLength(arguments[0]) is SqlExpression arrayLengthTranslation) - { - return _sqlExpressionFactory.GreaterThan(arrayLengthTranslation, _sqlExpressionFactory.Constant(0)); - } - - return 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 virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - if (instance?.TypeMapping is not NpgsqlJsonTypeMapping && instance is not PgJsonTraversalExpression) - { - return null; - } - - if (member is { Name: "Count", DeclaringType.IsGenericType: true } - && member.DeclaringType.GetGenericTypeDefinition() == typeof(List<>)) - { - return TranslateArrayLength(instance); - } - - return TranslateMemberAccess( - instance, - _sqlExpressionFactory.Constant(member.GetCustomAttribute()?.Name ?? member.Name), - returnType); - } - - /// - /// 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? TranslateMemberAccess(SqlExpression instance, SqlExpression member, Type returnType) - { - 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. - ColumnExpression { TypeMapping: NpgsqlJsonTypeMapping } columnExpression - => ConvertFromText( - _sqlExpressionFactory.JsonTraversal( - columnExpression, - [member], - returnsText: true, - typeof(string), - _stringTypeMapping), - returnType), - - PgJsonTraversalExpression prevPathTraversal - => ConvertFromText( - prevPathTraversal.Append(_sqlExpressionFactory.ApplyDefaultTypeMapping(member)), - returnType), - - _ => 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 - /// 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? TranslateArrayLength(SqlExpression expression) - { - switch (expression) - { - case ColumnExpression { TypeMapping: NpgsqlJsonTypeMapping mapping }: - return _sqlExpressionFactory.Function( - mapping.IsJsonb ? "jsonb_array_length" : "json_array_length", - [expression], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)); - - case PgJsonTraversalExpression traversal: - // The traversal expression has ReturnsText=true (e.g. ->> not ->), so we recreate it to return - // the JSON object instead. - var lastPathComponent = traversal.Path.Last(); - var newTraversal = new PgJsonTraversalExpression( - traversal.Expression, traversal.Path, - returnsText: false, - lastPathComponent.Type, - _typeMappingSource.FindMapping(lastPathComponent.Type, _model)); - - var jsonMapping = (NpgsqlJsonTypeMapping)traversal.Expression.TypeMapping!; - return _sqlExpressionFactory.Function( - jsonMapping.IsJsonb ? "jsonb_array_length" : "json_array_length", - [newTraversal], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)); - - default: - return null; - } - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs deleted file mode 100644 index 4875c22752..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLTreeTranslator.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlLTreeTranslator : IMethodCallTranslator, IMemberTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _boolTypeMapping; - private readonly RelationalTypeMapping _ltreeTypeMapping; - private readonly RelationalTypeMapping _lqueryTypeMapping; - private readonly RelationalTypeMapping _ltxtqueryTypeMapping; - - /// - /// 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 NpgsqlLTreeTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _sqlExpressionFactory = sqlExpressionFactory; - _boolTypeMapping = typeMappingSource.FindMapping(typeof(bool), model)!; - _ltreeTypeMapping = typeMappingSource.FindMapping(typeof(LTree), model)!; - _lqueryTypeMapping = typeMappingSource.FindMapping("lquery")!; - _ltxtqueryTypeMapping = typeMappingSource.FindMapping("ltxtquery")!; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType == typeof(LTree)) - { - return method.Name switch - { - nameof(LTree.IsAncestorOf) - => new PgBinaryExpression( - PgExpressionType.Contains, - ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), - ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), - typeof(bool), - _boolTypeMapping), - - nameof(LTree.IsDescendantOf) - => new PgBinaryExpression( - PgExpressionType.ContainedBy, - ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), - ApplyTypeMappingOrConvert(arguments[0], _ltreeTypeMapping), - typeof(bool), - _boolTypeMapping), - - nameof(LTree.MatchesLQuery) - => new PgBinaryExpression( - PgExpressionType.LTreeMatches, - ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), - ApplyTypeMappingOrConvert(arguments[0], _lqueryTypeMapping), - typeof(bool), - _boolTypeMapping), - - nameof(LTree.MatchesLTxtQuery) - => new PgBinaryExpression( - PgExpressionType.LTreeMatches, - ApplyTypeMappingOrConvert(instance!, _ltreeTypeMapping), - ApplyTypeMappingOrConvert(arguments[0], _ltxtqueryTypeMapping), - typeof(bool), - _boolTypeMapping), - - nameof(LTree.Subtree) - => _sqlExpressionFactory.Function( - "subltree", - [instance!, arguments[0], arguments[1]], - nullable: true, - TrueArrays[3], - typeof(LTree), - _ltreeTypeMapping), - - nameof(LTree.Subpath) - => _sqlExpressionFactory.Function( - "subpath", - arguments.Count == 2 - ? [instance!, arguments[0], arguments[1]] - : new[] { instance!, arguments[0] }, - nullable: true, - arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], - typeof(LTree), - _ltreeTypeMapping), - - nameof(LTree.Index) - => _sqlExpressionFactory.Function( - "index", - arguments.Count == 2 - ? [instance!, arguments[0], arguments[1]] - : new[] { instance!, arguments[0] }, - nullable: true, - arguments.Count == 2 ? TrueArrays[3] : TrueArrays[2], - typeof(int)), - - nameof(LTree.LongestCommonAncestor) - => _sqlExpressionFactory.Function( - "lca", - [arguments[0]], - nullable: true, - TrueArrays[1], - typeof(LTree), - _ltreeTypeMapping), - - _ => null - }; - } - - return 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 virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - => member.DeclaringType == typeof(LTree) && member.Name == nameof(LTree.NLevel) - ? _sqlExpressionFactory.Function( - "nlevel", - [instance!], - nullable: true, - TrueArrays[1], - typeof(int)) - : null; - - // Applying e.g. the LQuery type mapping on a function operator is a bit tricky. - // If it's a constant, we can just apply the mapping: the constant will get rendered as an untyped string literal, and PG will - // coerce it as the function parameter. - // If it's a parameter, we can also just apply the mapping (which causes NpgsqlDbType to be set to LQuery). - // For anything else, we may need an explicit cast to LQuery, e.g. a plain text column or a concatenation between strings; - // apply the default type mapping and then apply an additional Convert node if the resulting mapping isn't what we need. - private SqlExpression ApplyTypeMappingOrConvert(SqlExpression sqlExpression, RelationalTypeMapping typeMapping) - => sqlExpression is SqlConstantExpression or SqlParameterExpression - ? _sqlExpressionFactory.ApplyTypeMapping(sqlExpression, typeMapping) - : _sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpression) is var expressionWithDefaultTypeMapping - && expressionWithDefaultTypeMapping.TypeMapping!.StoreType == typeMapping.StoreType - ? expressionWithDefaultTypeMapping - : _sqlExpressionFactory.Convert(expressionWithDefaultTypeMapping, typeMapping.ClrType, typeMapping); -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs deleted file mode 100644 index 6aba0252d3..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlLikeTranslator.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Translates methods into PostgreSQL LIKE expressions. -/// -public class NpgsqlLikeTranslator : IMethodCallTranslator -{ - private static readonly MethodInfo Like = - typeof(DbFunctionsExtensions).GetRuntimeMethod( - nameof(DbFunctionsExtensions.Like), - [typeof(DbFunctions), typeof(string), typeof(string)])!; - - private static readonly MethodInfo LikeWithEscape = - typeof(DbFunctionsExtensions).GetRuntimeMethod( - nameof(DbFunctionsExtensions.Like), - [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; - - // ReSharper disable once InconsistentNaming - private static readonly MethodInfo ILike = - typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.ILike), - [typeof(DbFunctions), typeof(string), typeof(string)])!; - - // ReSharper disable once InconsistentNaming - private static readonly MethodInfo ILikeWithEscape = - typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.ILike), - [typeof(DbFunctions), typeof(string), typeof(string), typeof(string)])!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The SQL expression factory to use when generating expressions.. - public NpgsqlLikeTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method == LikeWithEscape) - { - return _sqlExpressionFactory.Like(arguments[1], arguments[2], arguments[3]); - } - - if (method == ILikeWithEscape) - { - return _sqlExpressionFactory.ILike(arguments[1], arguments[2], arguments[3]); - } - - bool sensitive; - if (method == Like) - { - sensitive = true; - } - else if (method == ILike) - { - sensitive = false; - } - else - { - return null; - } - - // PostgreSQL has backslash as the default LIKE escape character, but EF Core expects - // no escape character unless explicitly requested (https://github.com/aspnet/EntityFramework/issues/8696). - - // If we have a constant expression, we check that there are no backslashes in order to render with - // an ESCAPE clause (better SQL). If we have a constant expression with backslashes or a non-constant - // expression, we render an ESCAPE clause to disable backslash escaping. - - var (match, pattern) = (arguments[1], arguments[2]); - - if (pattern is SqlConstantExpression { Value: string patternValue } - && !patternValue.Contains('\\')) - { - return sensitive - ? _sqlExpressionFactory.Like(match, pattern) - : _sqlExpressionFactory.ILike(match, pattern); - } - - return sensitive - ? _sqlExpressionFactory.Like(match, pattern, _sqlExpressionFactory.Constant(string.Empty)) - : _sqlExpressionFactory.ILike(match, pattern, _sqlExpressionFactory.Constant(string.Empty)); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs deleted file mode 100644 index 07919734b1..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMathTranslator.cs +++ /dev/null @@ -1,290 +0,0 @@ -using System.Numerics; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; -using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translation services for static methods.. -/// -/// -/// 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 NpgsqlMathTranslator : IMethodCallTranslator -{ - private static readonly Dictionary SupportedMethodTranslations = new() - { - { 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), [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), [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), [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 = - [ - 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 = - [ - 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 = - [ - 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), [typeof(decimal), typeof(int)])!; - - private static readonly MethodInfo DoubleIsNanMethodInfo - = typeof(double).GetRuntimeMethod(nameof(double.IsNaN), [typeof(double)])!; - - private static readonly MethodInfo DoubleIsPositiveInfinityMethodInfo - = typeof(double).GetRuntimeMethod(nameof(double.IsPositiveInfinity), [typeof(double)])!; - - private static readonly MethodInfo DoubleIsNegativeInfinityMethodInfo - = typeof(double).GetRuntimeMethod(nameof(double.IsNegativeInfinity), [typeof(double)])!; - - private static readonly MethodInfo FloatIsNanMethodInfo - = typeof(float).GetRuntimeMethod(nameof(float.IsNaN), [typeof(float)])!; - - private static readonly MethodInfo FloatIsPositiveInfinityMethodInfo - = typeof(float).GetRuntimeMethod(nameof(float.IsPositiveInfinity), [typeof(float)])!; - - private static readonly MethodInfo FloatIsNegativeInfinityMethodInfo - = typeof(float).GetRuntimeMethod(nameof(float.IsNegativeInfinity), [typeof(float)])!; - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _intTypeMapping; - private readonly RelationalTypeMapping _decimalTypeMapping; - - /// - /// 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 NpgsqlMathTranslator( - IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _sqlExpressionFactory = sqlExpressionFactory; - _intTypeMapping = typeMappingSource.FindMapping(typeof(int), model)!; - _decimalTypeMapping = typeMappingSource.FindMapping(typeof(decimal), model)!; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (SupportedMethodTranslations.TryGetValue(method, out var sqlFunctionName)) - { - var typeMapping = arguments.Count == 1 - ? ExpressionExtensions.InferTypeMapping(arguments[0]) - : ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]); - - var newArguments = new SqlExpression[arguments.Count]; - newArguments[0] = _sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping); - - if (arguments.Count == 2) - { - newArguments[1] = _sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping); - } - - // Note: GREATER/LEAST only return NULL if *all* arguments are null, but we currently can't - // convey this. - return _sqlExpressionFactory.Function( - sqlFunctionName, - newArguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[newArguments.Length], - method.ReturnType, - typeMapping); - } - - if (TruncateMethodInfos.Contains(method)) - { - var argument = arguments[0]; - - // 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 = _sqlExpressionFactory.Function( - "trunc", - [argument], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - argument.Type == typeof(float) ? typeof(double) : argument.Type); - - if (argument.Type == typeof(float)) - { - result = _sqlExpressionFactory.Convert(result, typeof(float)); - } - - return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping); - } - - if (RoundMethodInfos.Contains(method)) - { - var argument = arguments[0]; - - // 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 = _sqlExpressionFactory.Function( - "round", - [argument], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - argument.Type == typeof(float) ? typeof(double) : argument.Type); - - if (argument.Type == typeof(float)) - { - result = _sqlExpressionFactory.Convert(result, typeof(float)); - } - - return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping); - } - - // PostgreSQL sign() returns 1, 0, -1, but in the same type as the argument, so we need to convert - // the return type to int. - if (SignMethodInfos.Contains(method)) - { - return - _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Function( - "sign", - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - method.ReturnType), - typeof(int), - _intTypeMapping); - } - - if (method == RoundDecimalTwoParams) - { - return _sqlExpressionFactory.Function( - "round", - [ - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]) - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - _decimalTypeMapping); - } - - // PostgreSQL treats NaN values as equal, against IEEE754 - if (method == DoubleIsNanMethodInfo) - { - return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(double.NaN)); - } - - if (method == FloatIsNanMethodInfo) - { - return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(float.NaN)); - } - - if (method == DoubleIsPositiveInfinityMethodInfo) - { - return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(double.PositiveInfinity)); - } - - if (method == FloatIsPositiveInfinityMethodInfo) - { - return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(float.PositiveInfinity)); - } - - if (method == DoubleIsNegativeInfinityMethodInfo) - { - return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(double.NegativeInfinity)); - } - - if (method == FloatIsNegativeInfinityMethodInfo) - { - return _sqlExpressionFactory.Equal(arguments[0], _sqlExpressionFactory.Constant(float.NegativeInfinity)); - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs deleted file mode 100644 index e7df363b9f..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// A composite member translator that dispatches to multiple specialized member translators specific to Npgsql. -/// -public class NpgsqlMemberTranslatorProvider : RelationalMemberTranslatorProvider -{ - /// - /// 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 NpgsqlJsonPocoTranslator JsonPocoTranslator { 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 NpgsqlMemberTranslatorProvider( - RelationalMemberTranslatorProviderDependencies dependencies, - IModel model, - IRelationalTypeMappingSource typeMappingSource, - IDbContextOptions contextOptions) - : base(dependencies) - { - var npgsqlOptions = contextOptions.FindExtension() ?? new NpgsqlOptionsExtension(); - var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14); - - var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory; - JsonPocoTranslator = new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model); - - AddTranslators( - [ - 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 NpgsqlCubeTranslator(sqlExpressionFactory, typeMappingSource) - ]); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs deleted file mode 100644 index 7f448c5405..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlMethodCallTranslatorProvider : RelationalMethodCallTranslatorProvider -{ - /// - /// 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 NpgsqlLTreeTranslator LTreeTranslator { 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 NpgsqlMethodCallTranslatorProvider( - RelationalMethodCallTranslatorProviderDependencies dependencies, - IModel model, - IDbContextOptions contextOptions) - : base(dependencies) - { - var npgsqlOptions = contextOptions.FindExtension() ?? new NpgsqlOptionsExtension(); - var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14); - var supportRegexCount = npgsqlOptions.PostgresVersion.AtLeast(15); - - var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory; - var typeMappingSource = (NpgsqlTypeMappingSource)dependencies.RelationalTypeMappingSource; - var jsonTranslator = new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model); - LTreeTranslator = new NpgsqlLTreeTranslator(typeMappingSource, sqlExpressionFactory, model); - - AddTranslators( - [ - new NpgsqlArrayMethodTranslator(sqlExpressionFactory, jsonTranslator), - new NpgsqlByteArrayMethodTranslator(sqlExpressionFactory), - new NpgsqlConvertTranslator(sqlExpressionFactory), - new NpgsqlDateTimeMethodTranslator(typeMappingSource, sqlExpressionFactory), - new NpgsqlFullTextSearchMethodTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlFuzzyStringMatchMethodTranslator(sqlExpressionFactory), - new NpgsqlJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlJsonDbFunctionsTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlLikeTranslator(sqlExpressionFactory), - LTreeTranslator, - new NpgsqlMathTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlNetworkTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlGuidTranslator(sqlExpressionFactory, npgsqlOptions.PostgresVersion), - new NpgsqlObjectToStringTranslator(typeMappingSource, sqlExpressionFactory), - new NpgsqlRandomTranslator(sqlExpressionFactory), - new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), - new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount), - new NpgsqlRowValueTranslator(sqlExpressionFactory), - new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory), - new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model), - new NpgsqlCubeTranslator(sqlExpressionFactory, typeMappingSource), - ]); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs deleted file mode 100644 index b938ecb96d..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMiscAggregateMethodTranslator.cs +++ /dev/null @@ -1,199 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlMiscAggregateMethodTranslator : IAggregateMethodCallTranslator -{ - private static readonly MethodInfo StringJoin - = typeof(string).GetRuntimeMethod(nameof(string.Join), [typeof(string), typeof(IEnumerable)])!; - - private static readonly MethodInfo StringConcat - = typeof(string).GetRuntimeMethod(nameof(string.Concat), [typeof(IEnumerable)])!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly 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 - /// 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 NpgsqlMiscAggregateMethodTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource, - IModel model) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - _model = 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 SqlExpression? Translate( - MethodInfo method, - EnumerableExpression source, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // Docs: https://www.postgresql.org/docs/current/functions-aggregate.html - - if (source.Selector is not SqlExpression sqlExpression) - { - return null; - } - - if (method == StringJoin || method == StringConcat) - { - // string_agg filters out nulls, but string.Join treats them as empty strings; coalesce unless we know we're aggregating over - // a non-nullable column. - if (sqlExpression is not ColumnExpression { IsNullable: false }) - { - sqlExpression = _sqlExpressionFactory.Coalesce( - sqlExpression, - _sqlExpressionFactory.Constant(string.Empty, typeof(string))); - } - - // string_agg returns null when there are no rows (or non-null values), but string.Join returns an empty string. - return _sqlExpressionFactory.Coalesce( - _sqlExpressionFactory.AggregateFunction( - "string_agg", - [ - sqlExpression, - method == StringJoin ? arguments[0] : _sqlExpressionFactory.Constant(string.Empty, typeof(string)) - ], - source, - nullable: 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))); - } - - if (method.DeclaringType == typeof(NpgsqlAggregateDbFunctionsExtensions)) - { - switch (method.Name) - { - case nameof(NpgsqlAggregateDbFunctionsExtensions.ArrayAgg): - return _sqlExpressionFactory.AggregateFunction( - "array_agg", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: method.ReturnType, - typeMapping: sqlExpression.TypeMapping is null - ? null - : _typeMappingSource.FindMapping(method.ReturnType, _model, sqlExpression.TypeMapping)); - - case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonAgg): - return _sqlExpressionFactory.AggregateFunction( - "json_agg", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, "json")); - - case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbAgg): - return _sqlExpressionFactory.AggregateFunction( - "jsonb_agg", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, "jsonb")); - - case nameof(NpgsqlAggregateDbFunctionsExtensions.Sum): - return _sqlExpressionFactory.AggregateFunction( - "sum", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: sqlExpression.Type, - sqlExpression.TypeMapping); - - case nameof(NpgsqlAggregateDbFunctionsExtensions.Average): - return _sqlExpressionFactory.AggregateFunction( - "avg", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: sqlExpression.Type, - sqlExpression.TypeMapping); - - case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbObjectAgg): - case nameof(NpgsqlAggregateDbFunctionsExtensions.JsonObjectAgg): - var isJsonb = method.Name == nameof(NpgsqlAggregateDbFunctionsExtensions.JsonbObjectAgg); - - // These methods accept two enumerable (column) arguments; this is represented in LINQ as a projection from the grouping - // to a tuple of the two columns. Since we generally translate tuples to PostgresRowValueExpression, we take it apart - // here. - if (source.Selector is not PgRowValueExpression rowValueExpression) - { - return null; - } - - var (keys, values) = (rowValueExpression.Values[0], rowValueExpression.Values[1]); - - return _sqlExpressionFactory.AggregateFunction( - isJsonb ? "jsonb_object_agg" : "json_object_agg", - [keys, values], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[2], - returnType: method.ReturnType, - _typeMappingSource.FindMapping(method.ReturnType, isJsonb ? "jsonb" : "json")); - } - } - - if (method.DeclaringType == typeof(NpgsqlRangeDbFunctionsExtensions)) - { - switch (method.Name) - { - case nameof(NpgsqlRangeDbFunctionsExtensions.RangeAgg): - var arrayClrType = sqlExpression.Type.MakeArrayType(); - - return _sqlExpressionFactory.AggregateFunction( - "range_agg", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: arrayClrType, - _typeMappingSource.FindMapping(arrayClrType)); - - case nameof(NpgsqlRangeDbFunctionsExtensions.RangeIntersectAgg): - return _sqlExpressionFactory.AggregateFunction( - "range_intersect_agg", - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: sqlExpression.Type, - sqlExpression.TypeMapping); - } - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs deleted file mode 100644 index 5dedf94da9..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlNetworkTranslator.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System.Net; -using System.Net.NetworkInformation; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translation services for operators and functions of PostgreSQL network typess (cidr, inet, macaddr, macaddr8). -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-net.html -/// -public class NpgsqlNetworkTranslator : IMethodCallTranslator -{ - private static readonly MethodInfo IPAddressParse = - typeof(IPAddress).GetRuntimeMethod(nameof(IPAddress.Parse), [typeof(string)])!; - - private static readonly MethodInfo PhysicalAddressParse = - typeof(PhysicalAddress).GetRuntimeMethod(nameof(PhysicalAddress.Parse), [typeof(string)])!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - private readonly RelationalTypeMapping _inetMapping; - private readonly RelationalTypeMapping _cidrMapping; - private readonly RelationalTypeMapping _macaddr8Mapping; - private readonly RelationalTypeMapping _longAddressMapping; - - /// - /// 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 NpgsqlNetworkTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _sqlExpressionFactory = sqlExpressionFactory; - _inetMapping = typeMappingSource.FindMapping("inet")!; - _cidrMapping = typeMappingSource.FindMapping("cidr")!; - _macaddr8Mapping = typeMappingSource.FindMapping("macaddr8")!; - _longAddressMapping = typeMappingSource.FindMapping(typeof(long), model)!; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method == IPAddressParse) - { - return _sqlExpressionFactory.Convert(arguments[0], typeof(IPAddress)); - } - - if (method == PhysicalAddressParse) - { - return _sqlExpressionFactory.Convert(arguments[0], typeof(PhysicalAddress)); - } - - if (method.DeclaringType == typeof(NpgsqlNetworkDbFunctionsExtensions)) - { - var paramType = method.GetParameters()[1].ParameterType; - - if (paramType == typeof(NpgsqlInet)) - { - return TranslateInetExtensionMethod(method, arguments); - } - -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - if (paramType == typeof(IPNetwork) || paramType == typeof(NpgsqlCidr)) - { - return TranslateCidrExtensionMethod(method, arguments); - } -#pragma warning restore CS0618 - - if (paramType == typeof(PhysicalAddress)) - { - return TranslateMacaddrExtensionMethod(method, arguments); - } - } - - return null; - } - - private SqlExpression? TranslateInetExtensionMethod(MethodInfo method, IReadOnlyList arguments) - => method.Name switch - { - nameof(NpgsqlNetworkDbFunctionsExtensions.LessThan) - => new SqlBinaryExpression( - ExpressionType.LessThan, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.LessThanOrEqual) - => new SqlBinaryExpression( - ExpressionType.LessThanOrEqual, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.GreaterThanOrEqual) - => new SqlBinaryExpression( - ExpressionType.GreaterThanOrEqual, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.GreaterThan) - => new SqlBinaryExpression( - ExpressionType.GreaterThan, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.ContainedBy) - => _sqlExpressionFactory.ContainedBy(arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.ContainedByOrEqual) - => _sqlExpressionFactory.MakePostgresBinary( - PgExpressionType.NetworkContainedByOrEqual, arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.Contains) - => _sqlExpressionFactory.Contains(arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.ContainsOrEqual) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.NetworkContainsOrEqual, arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.ContainsOrContainedBy) - => _sqlExpressionFactory.MakePostgresBinary( - PgExpressionType.NetworkContainsOrContainedBy, arguments[1], arguments[2]), - - nameof(NpgsqlNetworkDbFunctionsExtensions.BitwiseNot) - => new SqlUnaryExpression( - ExpressionType.Not, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.BitwiseAnd) - => new SqlBinaryExpression( - ExpressionType.And, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.BitwiseOr) - => new SqlBinaryExpression( - ExpressionType.Or, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.Add) - => new SqlBinaryExpression( - ExpressionType.Add, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.Subtract) when arguments[2].Type == typeof(long) - => new SqlBinaryExpression( - ExpressionType.Subtract, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(NpgsqlInet), - _inetMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.Subtract) - => new SqlBinaryExpression( - ExpressionType.Subtract, - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - typeof(long), - _longAddressMapping), - - nameof(NpgsqlNetworkDbFunctionsExtensions.Abbreviate) - => NullPropagatingFunction("abbrev", [arguments[1]], typeof(string)), - nameof(NpgsqlNetworkDbFunctionsExtensions.Broadcast) - => NullPropagatingFunction("broadcast", [arguments[1]], typeof(IPAddress), _inetMapping), - nameof(NpgsqlNetworkDbFunctionsExtensions.Family) - => NullPropagatingFunction("family", [arguments[1]], typeof(int)), - nameof(NpgsqlNetworkDbFunctionsExtensions.Host) - => NullPropagatingFunction("host", [arguments[1]], typeof(string)), - nameof(NpgsqlNetworkDbFunctionsExtensions.HostMask) - => NullPropagatingFunction("hostmask", [arguments[1]], typeof(IPAddress), _inetMapping), - nameof(NpgsqlNetworkDbFunctionsExtensions.MaskLength) - => NullPropagatingFunction("masklen", [arguments[1]], typeof(int)), - nameof(NpgsqlNetworkDbFunctionsExtensions.Netmask) - => NullPropagatingFunction("netmask", [arguments[1]], typeof(IPAddress), _inetMapping), - nameof(NpgsqlNetworkDbFunctionsExtensions.Network) - => NullPropagatingFunction("network", [arguments[1]], typeof((IPAddress Address, int Subnet)), _cidrMapping), - nameof(NpgsqlNetworkDbFunctionsExtensions.SetMaskLength) - => NullPropagatingFunction( - "set_masklen", [arguments[1], arguments[2]], arguments[1].Type, arguments[1].TypeMapping), - nameof(NpgsqlNetworkDbFunctionsExtensions.Text) - => NullPropagatingFunction("text", [arguments[1]], typeof(string)), - nameof(NpgsqlNetworkDbFunctionsExtensions.SameFamily) - => NullPropagatingFunction("inet_same_family", [arguments[1], arguments[2]], typeof(bool)), - nameof(NpgsqlNetworkDbFunctionsExtensions.Merge) - => NullPropagatingFunction( - "inet_merge", [arguments[1], arguments[2]], typeof((IPAddress Address, int Subnet)), _cidrMapping), - - _ => null - }; - - private SqlExpression? TranslateCidrExtensionMethod(MethodInfo method, IReadOnlyList arguments) - => method.Name switch - { - nameof(NpgsqlNetworkDbFunctionsExtensions.Abbreviate) - => NullPropagatingFunction("abbrev", [arguments[1]], typeof(string)), - nameof(NpgsqlNetworkDbFunctionsExtensions.SetMaskLength) - => NullPropagatingFunction( - "set_masklen", [arguments[1], arguments[2]], arguments[1].Type, arguments[1].TypeMapping), - - _ => null - }; - - private SqlExpression? TranslateMacaddrExtensionMethod(MethodInfo method, IReadOnlyList arguments) - => method.Name switch - { - nameof(NpgsqlNetworkDbFunctionsExtensions.LessThan) - => _sqlExpressionFactory.LessThan(arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.LessThanOrEqual) - => _sqlExpressionFactory.LessThanOrEqual(arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.GreaterThanOrEqual) - => _sqlExpressionFactory.GreaterThanOrEqual(arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.GreaterThan) - => _sqlExpressionFactory.GreaterThan(arguments[1], arguments[2]), - - nameof(NpgsqlNetworkDbFunctionsExtensions.BitwiseNot) - => _sqlExpressionFactory.Not(arguments[1]), - nameof(NpgsqlNetworkDbFunctionsExtensions.BitwiseAnd) - => _sqlExpressionFactory.And(arguments[1], arguments[2]), - nameof(NpgsqlNetworkDbFunctionsExtensions.BitwiseOr) - => _sqlExpressionFactory.Or(arguments[1], arguments[2]), - - nameof(NpgsqlNetworkDbFunctionsExtensions.Truncate) => NullPropagatingFunction( - "trunc", [arguments[1]], typeof(PhysicalAddress), arguments[1].TypeMapping), - nameof(NpgsqlNetworkDbFunctionsExtensions.Set7BitMac8) => NullPropagatingFunction( - "macaddr8_set7bit", [arguments[1]], typeof(PhysicalAddress), _macaddr8Mapping), - - _ => null - }; - - private SqlExpression NullPropagatingFunction( - string name, - SqlExpression[] arguments, - Type returnType, - RelationalTypeMapping? typeMapping = null) - => _sqlExpressionFactory.Function( - name, - arguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Length], - returnType, - typeMapping); -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs deleted file mode 100644 index 2a98b58ae5..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlObjectToStringTranslator.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlObjectToStringTranslator : IMethodCallTranslator -{ - private static readonly HashSet _typeMapping = - [ - typeof(int), - typeof(long), - typeof(DateTime), - typeof(Guid), - typeof(bool), - typeof(byte), - //typeof(byte[]) - typeof(double), - typeof(DateTimeOffset), - typeof(char), - typeof(short), - typeof(float), - typeof(decimal), - typeof(TimeSpan), - typeof(uint), - typeof(ushort), - typeof(ulong), - typeof(sbyte) - ]; - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _textTypeMapping; - - /// - /// 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 NpgsqlObjectToStringTranslator(IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - - _textTypeMapping = typeMappingSource.FindMapping("text")!; - } - - /// - /// 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? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (instance is null || method.Name != nameof(ToString) || arguments.Count != 0) - { - return null; - } - - if (instance.Type == typeof(bool)) - { - 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( - instance, - _sqlExpressionFactory.Constant(true.ToString())) - ], - _sqlExpressionFactory.Constant(false.ToString())); - } - } - - return _typeMapping.Contains(instance.Type) - || instance.Type.UnwrapNullableType().IsEnum && instance.TypeMapping is NpgsqlEnumTypeMapping - ? _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 deleted file mode 100644 index 1cffc45fe8..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlQueryableAggregateMethodTranslator.cs +++ /dev/null @@ -1,186 +0,0 @@ -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlQueryableAggregateMethodTranslator : IAggregateMethodCallTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - - /// - /// 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 NpgsqlQueryableAggregateMethodTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _typeMappingSource = typeMappingSource; - } - - /// - /// 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? Translate( - MethodInfo method, - EnumerableExpression source, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType == typeof(Queryable)) - { - var methodInfo = method.IsGenericMethod - ? method.GetGenericMethodDefinition() - : method; - switch (methodInfo.Name) - { - case nameof(Queryable.Average) - when (QueryableMethods.IsAverageWithoutSelector(methodInfo) - || QueryableMethods.IsAverageWithSelector(methodInfo)) - && source.Selector is SqlExpression averageSqlExpression: - var averageInputType = averageSqlExpression.Type; - if (averageInputType == typeof(int) - || averageInputType == typeof(long)) - { - averageSqlExpression = _sqlExpressionFactory.ApplyDefaultTypeMapping( - _sqlExpressionFactory.Convert(averageSqlExpression, typeof(double))); - } - - return averageInputType == typeof(float) - ? _sqlExpressionFactory.Convert( - _sqlExpressionFactory.AggregateFunction( - "avg", - [averageSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - returnType: typeof(double)), - averageSqlExpression.Type, - averageSqlExpression.TypeMapping) - : _sqlExpressionFactory.AggregateFunction( - "avg", - [averageSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - averageSqlExpression.Type, - averageSqlExpression.TypeMapping); - - // PostgreSQL COUNT() always returns bigint, so we need to downcast to int - case nameof(Queryable.Count) - when methodInfo == QueryableMethods.CountWithoutPredicate - || methodInfo == QueryableMethods.CountWithPredicate: - var countSqlExpression = (source.Selector as SqlExpression) ?? _sqlExpressionFactory.Fragment("*"); - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.AggregateFunction( - "count", - [countSqlExpression], - source, - nullable: false, - argumentsPropagateNullability: FalseArrays[1], - typeof(long)), - typeof(int), - _typeMappingSource.FindMapping(typeof(int))); - - case nameof(Queryable.LongCount) - when methodInfo == QueryableMethods.LongCountWithoutPredicate - || methodInfo == QueryableMethods.LongCountWithPredicate: - var longCountSqlExpression = (source.Selector as SqlExpression) ?? _sqlExpressionFactory.Fragment("*"); - return _sqlExpressionFactory.AggregateFunction( - "count", - [longCountSqlExpression], - source, - nullable: false, - argumentsPropagateNullability: FalseArrays[1], - typeof(long)); - - case nameof(Queryable.Max) - when (methodInfo == QueryableMethods.MaxWithoutSelector - || methodInfo == QueryableMethods.MaxWithSelector) - && source.Selector is SqlExpression maxSqlExpression: - return _sqlExpressionFactory.AggregateFunction( - "max", - [maxSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - maxSqlExpression.Type, - maxSqlExpression.TypeMapping); - - case nameof(Queryable.Min) - when (methodInfo == QueryableMethods.MinWithoutSelector - || methodInfo == QueryableMethods.MinWithSelector) - && source.Selector is SqlExpression minSqlExpression: - return _sqlExpressionFactory.AggregateFunction( - "min", - [minSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - minSqlExpression.Type, - minSqlExpression.TypeMapping); - - // In PostgreSQL SUM() doesn't return the same type as its argument for smallint, int and bigint. - // Cast to get the same type. - // http://www.postgresql.org/docs/current/static/functions-aggregate.html - case nameof(Queryable.Sum) - when (QueryableMethods.IsSumWithoutSelector(methodInfo) - || QueryableMethods.IsSumWithSelector(methodInfo)) - && source.Selector is SqlExpression sumSqlExpression: - var sumInputType = sumSqlExpression.Type; - - // Note that there is no Sum over short in LINQ - if (sumInputType == typeof(int)) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.AggregateFunction( - "sum", - [sumSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - typeof(long)), - sumInputType, - sumSqlExpression.TypeMapping); - } - - if (sumInputType == typeof(long)) - { - return _sqlExpressionFactory.Convert( - _sqlExpressionFactory.AggregateFunction( - "sum", - [sumSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - typeof(decimal)), - sumInputType, - sumSqlExpression.TypeMapping); - } - - return _sqlExpressionFactory.AggregateFunction( - "sum", - [sumSqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - sumInputType, - sumSqlExpression.TypeMapping); - } - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs deleted file mode 100644 index 3368c9a65e..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRandomTranslator.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlRandomTranslator : IMethodCallTranslator -{ - private static readonly MethodInfo _methodInfo - = typeof(DbFunctionsExtensions).GetRuntimeMethod(nameof(DbFunctionsExtensions.Random), [typeof(DbFunctions)])!; - - 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 NpgsqlRandomTranslator(ISqlExpressionFactory sqlExpressionFactory) - { - _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. - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - Check.NotNull(method, nameof(method)); - Check.NotNull(arguments, nameof(arguments)); - Check.NotNull(logger, nameof(logger)); - - return _methodInfo.Equals(method) - ? _sqlExpressionFactory.Function( - "random", - [], - nullable: false, - argumentsPropagateNullability: [], - method.ReturnType) - : null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs deleted file mode 100644 index 2b6747fdf3..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRangeTranslator.cs +++ /dev/null @@ -1,181 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -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; - -/// -/// 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 NpgsqlRangeTranslator : IMethodCallTranslator, IMemberTranslator -{ - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IModel _model; - private readonly bool _supportsMultiranges; - - private static readonly MethodInfo EnumerableAnyWithoutPredicate = - typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1); - - /// - /// 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 NpgsqlRangeTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory npgsqlSqlExpressionFactory, - IModel model, - bool supportsMultiranges) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = npgsqlSqlExpressionFactory; - _model = model; - _supportsMultiranges = supportsMultiranges; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // Any() over multirange -> NOT isempty(). NpgsqlRange has IsEmpty which is translated below. - if (_supportsMultiranges - && method.IsGenericMethod - && method.GetGenericMethodDefinition() == EnumerableAnyWithoutPredicate - && arguments[0].IsMultirange()) - { - return _sqlExpressionFactory.Not( - _sqlExpressionFactory.Function( - "isempty", - [arguments[0]], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(bool))); - } - - if (method.DeclaringType != typeof(NpgsqlRangeDbFunctionsExtensions) - && (method.DeclaringType != typeof(NpgsqlMultirangeDbFunctionsExtensions) || !_supportsMultiranges)) - { - return null; - } - - if (method.Name == nameof(NpgsqlRangeDbFunctionsExtensions.Merge)) - { - if (method.DeclaringType == typeof(NpgsqlRangeDbFunctionsExtensions)) - { - var inferredMapping = ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]); - - return _sqlExpressionFactory.Function( - "range_merge", - [ - _sqlExpressionFactory.ApplyTypeMapping(arguments[0], inferredMapping), - _sqlExpressionFactory.ApplyTypeMapping(arguments[1], inferredMapping) - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType, - inferredMapping); - } - - if (method.DeclaringType == typeof(NpgsqlMultirangeDbFunctionsExtensions)) - { - var returnTypeMapping = arguments[0].TypeMapping is NpgsqlMultirangeTypeMapping multirangeTypeMapping - ? multirangeTypeMapping.RangeMapping - : null; - - return _sqlExpressionFactory.Function( - "range_merge", - [arguments[0]], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - method.ReturnType, - returnTypeMapping); - } - } - - return method.Name switch - { - nameof(NpgsqlRangeDbFunctionsExtensions.Contains) - => _sqlExpressionFactory.Contains(arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.ContainedBy) - => _sqlExpressionFactory.ContainedBy(arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.Overlaps) - => _sqlExpressionFactory.Overlaps(arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.IsStrictlyLeftOf) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeIsStrictlyLeftOf, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.IsStrictlyRightOf) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeIsStrictlyRightOf, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.DoesNotExtendRightOf) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeDoesNotExtendRightOf, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.DoesNotExtendLeftOf) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeDoesNotExtendLeftOf, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.IsAdjacentTo) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeIsAdjacentTo, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.Union) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeUnion, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.Intersect) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeIntersect, arguments[0], arguments[1]), - nameof(NpgsqlRangeDbFunctionsExtensions.Except) - => _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.RangeExcept, arguments[0], arguments[1]), - - _ => null - }; - } - - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - var type = member.DeclaringType; - if (type is null || !type.IsGenericType || type.GetGenericTypeDefinition() != typeof(NpgsqlRange<>)) - { - return null; - } - - if (member.Name is nameof(NpgsqlRange.LowerBound) or nameof(NpgsqlRange.UpperBound)) - { - var typeMapping = instance!.TypeMapping is NpgsqlRangeTypeMapping rangeMapping - ? rangeMapping.SubtypeMapping - : _typeMappingSource.FindMapping(returnType, _model); - - return _sqlExpressionFactory.Function( - member.Name == nameof(NpgsqlRange.LowerBound) ? "lower" : "upper", - [instance], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - returnType, - typeMapping); - } - - return member.Name switch - { - nameof(NpgsqlRange.IsEmpty) => SingleArgBoolFunction("isempty", instance!), - nameof(NpgsqlRange.LowerBoundIsInclusive) => SingleArgBoolFunction("lower_inc", instance!), - nameof(NpgsqlRange.UpperBoundIsInclusive) => SingleArgBoolFunction("upper_inc", instance!), - nameof(NpgsqlRange.LowerBoundInfinite) => SingleArgBoolFunction("lower_inf", instance!), - nameof(NpgsqlRange.UpperBoundInfinite) => SingleArgBoolFunction("upper_inf", instance!), - - _ => null - }; - - SqlExpression SingleArgBoolFunction(string name, SqlExpression argument) - => _sqlExpressionFactory.Function( - name, - [argument], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(bool)); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs deleted file mode 100644 index 3407bef163..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Translates Regex method calls into their corresponding PostgreSQL equivalent for database-side processing. -/// -/// -/// http://www.postgresql.org/docs/current/static/functions-matching.html -/// -public class NpgsqlRegexTranslator : IMethodCallTranslator -{ - private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly bool _supportRegexCount; - private readonly NpgsqlTypeMappingSource _typeMappingSource; - - /// - /// 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 NpgsqlRegexTranslator( - NpgsqlTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - bool supportRegexCount) - { - _sqlExpressionFactory = sqlExpressionFactory; - _supportRegexCount = supportRegexCount; - _typeMappingSource = typeMappingSource; - } - - /// - public SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType != typeof(Regex) || !method.IsStatic) - { - return null; - } - - return method.Name switch - { - nameof(Regex.IsMatch) when arguments.Count == 2 - && arguments[0].Type == typeof(string) - && arguments[1].Type == typeof(string) - => TranslateIsMatch(arguments), - nameof(Regex.IsMatch) when arguments.Count == 3 - && arguments[0].Type == typeof(string) - && arguments[1].Type == typeof(string) - && TryGetOptions(arguments[2], out var options) - => TranslateIsMatch(arguments, options), - - nameof(Regex.Replace) when arguments.Count == 3 - && arguments[0].Type == typeof(string) - && arguments[1].Type == typeof(string) - && arguments[2].Type == typeof(string) - => TranslateReplace(arguments), - nameof(Regex.Replace) when arguments.Count == 4 - && arguments[0].Type == typeof(string) - && arguments[1].Type == typeof(string) - && arguments[2].Type == typeof(string) - && TryGetOptions(arguments[3], out var options) - => TranslateReplace(arguments, options), - - nameof(Regex.Count) when _supportRegexCount - && arguments.Count == 2 - && arguments[0].Type == typeof(string) - && arguments[1].Type == typeof(string) - => TranslateCount(arguments), - nameof(Regex.Count) when _supportRegexCount - && arguments.Count == 3 - && arguments[0].Type == typeof(string) - && arguments[1].Type == typeof(string) - && TryGetOptions(arguments[2], out var options) - => TranslateCount(arguments, options), - - _ => null - }; - - static bool TryGetOptions(SqlExpression argument, out RegexOptions options) - { - if (argument is SqlConstantExpression { Value: RegexOptions o } && (o & UnsupportedRegexOptions) is 0) - { - options = o; - return true; - } - - options = default; - return false; - } - } - - private PgRegexMatchExpression TranslateIsMatch(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) - => _sqlExpressionFactory.RegexMatch( - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - regexOptions); - - private SqlExpression TranslateReplace(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) - { - var (input, pattern, replacement) = (arguments[0], arguments[1], arguments[2]); - - List passingArguments = - [ - _sqlExpressionFactory.ApplyDefaultTypeMapping(input), - _sqlExpressionFactory.ApplyDefaultTypeMapping(pattern), - _sqlExpressionFactory.ApplyDefaultTypeMapping(replacement) - ]; - - if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions) - { - passingArguments.Add(_sqlExpressionFactory.Constant(translatedOptions)); - } - - return _sqlExpressionFactory.Function( - "regexp_replace", - passingArguments, - nullable: true, - TrueArrays[passingArguments.Count], - typeof(string), - _typeMappingSource.FindMapping(typeof(string))); - } - - private SqlExpression TranslateCount(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) - { - var (input, pattern) = (arguments[0], arguments[1]); - - List passingArguments = - [ - _sqlExpressionFactory.ApplyDefaultTypeMapping(input), - _sqlExpressionFactory.ApplyDefaultTypeMapping(pattern) - ]; - - if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions) - { - passingArguments.AddRange( - [ - _sqlExpressionFactory.Constant(1), // The starting position has to be set to use the options in PostgreSQL - _sqlExpressionFactory.Constant(translatedOptions) - ]); - } - - return _sqlExpressionFactory.Function( - "regexp_count", - passingArguments, - nullable: true, - TrueArrays[passingArguments.Count], - typeof(int), - _typeMappingSource.FindMapping(typeof(int))); - } - - private static string TranslateOptions(RegexOptions options) - { - string? result; - - switch (options) - { - case RegexOptions.Singleline: - return string.Empty; - case var _ when options.HasFlag(RegexOptions.Multiline): - result = "n"; - break; - case var _ when !options.HasFlag(RegexOptions.Singleline): - result = "p"; - break; - default: - result = string.Empty; - break; - } - - if (options.HasFlag(RegexOptions.IgnoreCase)) - { - result += "i"; - } - - if (options.HasFlag(RegexOptions.IgnorePatternWhitespace)) - { - result += "x"; - } - - return result; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs deleted file mode 100644 index f1e6f3aebd..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRowValueTranslator.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlRowValueTranslator : IMethodCallTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - - private static readonly MethodInfo GreaterThan = - typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.GreaterThan), - [typeof(DbFunctions), typeof(ITuple), typeof(ITuple)])!; - - private static readonly MethodInfo LessThan = - typeof(NpgsqlDbFunctionsExtensions).GetMethods() - .Single(m => m.Name == nameof(NpgsqlDbFunctionsExtensions.LessThan)); - - private static readonly MethodInfo GreaterThanOrEqual = - typeof(NpgsqlDbFunctionsExtensions).GetMethods() - .Single(m => m.Name == nameof(NpgsqlDbFunctionsExtensions.GreaterThanOrEqual)); - - private static readonly MethodInfo LessThanOrEqual = - typeof(NpgsqlDbFunctionsExtensions).GetMethods() - .Single(m => m.Name == nameof(NpgsqlDbFunctionsExtensions.LessThanOrEqual)); - - private static readonly Dictionary ComparisonMethods = new() - { - { GreaterThan, ExpressionType.GreaterThan }, - { LessThan, ExpressionType.LessThan }, - { GreaterThanOrEqual, ExpressionType.GreaterThanOrEqual }, - { LessThanOrEqual, ExpressionType.LessThanOrEqual } - }; - - /// - /// Initializes a new instance of the class. - /// - public NpgsqlRowValueTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(ValueType))] // For ValueTuple.Create - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // Translate ValueTuple.Create - if (method.DeclaringType == typeof(ValueTuple) && method is { IsStatic: true, Name: nameof(ValueTuple.Create) }) - { - return new PgRowValueExpression(arguments, method.ReturnType); - } - - // Translate EF.Functions.GreaterThan and other comparisons - if (method.DeclaringType != typeof(NpgsqlDbFunctionsExtensions) || !ComparisonMethods.TryGetValue(method, out var expressionType)) - { - return null; - } - - var leftCount = arguments[1] is PgRowValueExpression leftRowValue - ? leftRowValue.Values.Count - : arguments[1] is SqlConstantExpression { Value : ITuple leftTuple } - ? (int?)leftTuple.Length - : null; - - var rightCount = arguments[2] is PgRowValueExpression rightRowValue - ? rightRowValue.Values.Count - : arguments[2] is SqlConstantExpression { Value : ITuple rightTuple } - ? (int?)rightTuple.Length - : null; - - if (leftCount is null || rightCount is null) - { - return null; - } - - if (leftCount != rightCount) - { - throw new ArgumentException(NpgsqlStrings.RowValueComparisonRequiresTuplesOfSameLength); - } - - return _sqlExpressionFactory.MakeBinary(expressionType, arguments[1], arguments[2], typeMapping: null); - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs deleted file mode 100644 index 1bb84be83f..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStatisticsAggregateMethodTranslator.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlStatisticsAggregateMethodTranslator : IAggregateMethodCallTranslator -{ - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _doubleTypeMapping, _longTypeMapping; - - /// - /// 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 NpgsqlStatisticsAggregateMethodTranslator( - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IRelationalTypeMappingSource typeMappingSource) - { - _sqlExpressionFactory = sqlExpressionFactory; - _doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!; - _longTypeMapping = typeMappingSource.FindMapping(typeof(long))!; - } - - /// - /// 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? Translate( - MethodInfo method, - EnumerableExpression source, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - // Docs: https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE-STATISTICS-TABLE - - if (method.DeclaringType != typeof(NpgsqlAggregateDbFunctionsExtensions) - || source.Selector is not SqlExpression sqlExpression) - { - return null; - } - - // These four functions are simple and take a single enumerable argument - var functionName = method.Name switch - { - nameof(NpgsqlAggregateDbFunctionsExtensions.StandardDeviationSample) => "stddev_samp", - nameof(NpgsqlAggregateDbFunctionsExtensions.StandardDeviationPopulation) => "stddev_pop", - nameof(NpgsqlAggregateDbFunctionsExtensions.VarianceSample) => "var_samp", - nameof(NpgsqlAggregateDbFunctionsExtensions.VariancePopulation) => "var_pop", - _ => null - }; - - if (functionName is not null) - { - return _sqlExpressionFactory.AggregateFunction( - functionName, - [sqlExpression], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[1], - typeof(double), - _doubleTypeMapping); - } - - functionName = method.Name switch - { - nameof(NpgsqlAggregateDbFunctionsExtensions.Correlation) => "corr", - nameof(NpgsqlAggregateDbFunctionsExtensions.CovariancePopulation) => "covar_pop", - nameof(NpgsqlAggregateDbFunctionsExtensions.CovarianceSample) => "covar_samp", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrAverageX) => "regr_avgx", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrAverageY) => "regr_avgy", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrCount) => "regr_count", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrIntercept) => "regr_intercept", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrR2) => "regr_r2", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrSlope) => "regr_slope", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrSXX) => "regr_sxx", - nameof(NpgsqlAggregateDbFunctionsExtensions.RegrSXY) => "regr_sxy", - _ => null - }; - - if (functionName is not null) - { - // These methods accept two enumerable (column) arguments; this is represented in LINQ as a projection from the grouping - // to a tuple of the two columns. Since we generally translate tuples to PostgresRowValueExpression, we take it apart here. - if (source.Selector is not PgRowValueExpression rowValueExpression) - { - return null; - } - - var (y, x) = (rowValueExpression.Values[0], rowValueExpression.Values[1]); - - return method.Name == nameof(NpgsqlAggregateDbFunctionsExtensions.RegrCount) - ? _sqlExpressionFactory.AggregateFunction( - functionName, - [y, x], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[2], - typeof(long), - _longTypeMapping) - : _sqlExpressionFactory.AggregateFunction( - functionName, - [y, x], - source, - nullable: true, - argumentsPropagateNullability: FalseArrays[2], - typeof(double), - _doubleTypeMapping); - } - - return null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs deleted file mode 100644 index a64f931b16..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMemberTranslator.cs +++ /dev/null @@ -1,44 +0,0 @@ -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Translates to 'length(text)'. -/// -public class NpgsqlStringMemberTranslator : IMemberTranslator -{ - 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 NpgsqlStringMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) - { - _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. - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - => member.Name == nameof(string.Length) && member.DeclaringType == typeof(string) - ? _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Function( - "length", - [instance!], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(long)), - returnType) - : null; -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs deleted file mode 100644 index d56f63380b..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlStringMethodTranslator.cs +++ /dev/null @@ -1,461 +0,0 @@ -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; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Provides translation services for PostgreSQL string functions. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-string.html -/// -public class NpgsqlStringMethodTranslator : IMethodCallTranslator -{ - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly SqlExpression _whitespace; - - #region MethodInfo - - 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), [typeof(string)])!; - - private static readonly MethodInfo PadLeft = typeof(string).GetRuntimeMethod(nameof(string.PadLeft), [typeof(int)])!; - - private static readonly MethodInfo PadLeftWithChar = typeof(string).GetRuntimeMethod( - nameof(string.PadLeft), [typeof(int), typeof(char)])!; - - private static readonly MethodInfo PadRight = typeof(string).GetRuntimeMethod(nameof(string.PadRight), [typeof(int)])!; - - private static readonly MethodInfo PadRightWithChar = typeof(string).GetRuntimeMethod( - nameof(string.PadRight), [typeof(int), typeof(char)])!; - - private static readonly MethodInfo Replace = typeof(string).GetRuntimeMethod( - 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); - - 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), [])!; - 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), [typeof(char[])])!; - - private static readonly MethodInfo TrimBothWithSingleChar = - 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), [typeof(char[])])!; - - private static readonly MethodInfo TrimEndWithSingleChar = - 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), [typeof(char[])])!; - - private static readonly MethodInfo TrimStartWithSingleChar = - typeof(string).GetRuntimeMethod(nameof(string.TrimStart), [typeof(char)])!; - - private static readonly MethodInfo Reverse = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.Reverse), [typeof(DbFunctions), typeof(string)])!; - - private static readonly MethodInfo StringToArray = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.StringToArray), [typeof(DbFunctions), typeof(string), typeof(string)])!; - - private static readonly MethodInfo StringToArrayNullString = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - 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( - m => m.Name == nameof(Enumerable.FirstOrDefault) - && m.GetParameters().Length == 1).MakeGenericMethod(typeof(char)); - - private static readonly MethodInfo LastOrDefaultMethodInfoWithoutArgs - = typeof(Enumerable).GetRuntimeMethods().Single( - m => m.Name == nameof(Enumerable.LastOrDefault) - && m.GetParameters().Length == 1).MakeGenericMethod(typeof(char)); - - // ReSharper disable InconsistentNaming - private static readonly MethodInfo String_Join1 = - typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(object[])])!; - - private static readonly MethodInfo String_Join2 = - typeof(string).GetMethod(nameof(string.Join), [typeof(string), typeof(string[])])!; - - private static readonly MethodInfo String_Join3 = - typeof(string).GetMethod(nameof(string.Join), [typeof(char), typeof(object[])])!; - - private static readonly MethodInfo String_Join4 = - 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) - .Single( - m => m is { Name: nameof(string.Join), IsGenericMethod: true } - && m.GetParameters().Length == 2 - && m.GetParameters()[0].ParameterType == typeof(string)); - - private static readonly MethodInfo String_Join_generic2 = - typeof(string).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single( - m => m is { Name: nameof(string.Join), IsGenericMethod: true } - && m.GetParameters().Length == 2 - && m.GetParameters()[0].ParameterType == typeof(char)); - // ReSharper restore InconsistentNaming - - #endregion - - /// - /// 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 NpgsqlStringMethodTranslator(NpgsqlTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory) - { - _typeMappingSource = typeMappingSource; - _sqlExpressionFactory = sqlExpressionFactory; - _whitespace = _sqlExpressionFactory.Constant( - @" \t\n\r", // TODO: Complete this - typeMappingSource.EStringTypeMapping); - } - - /// - /// 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? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method.DeclaringType == typeof(string)) - { - return TranslateStringMethod(instance, method, arguments); - } - - if (method.DeclaringType == typeof(NpgsqlDbFunctionsExtensions)) - { - return TranslateDbFunctionsMethod(instance, method, arguments); - } - - if (method == FirstOrDefaultMethodInfoWithoutArgs) - { - var argument = arguments[0]; - return _sqlExpressionFactory.Function( - "substr", - [argument, _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1)], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - method.ReturnType); - } - - if (method == LastOrDefaultMethodInfoWithoutArgs) - { - var argument = arguments[0]; - return _sqlExpressionFactory.Function( - "substr", - [ - argument, - _sqlExpressionFactory.Function( - "length", - [argument], - nullable: true, - argumentsPropagateNullability: [true], - typeof(int)), - _sqlExpressionFactory.Constant(1) - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - method.ReturnType); - } - - return null; - } - - private SqlExpression? TranslateStringMethod(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments) - { - if (method == IndexOfString || method == IndexOfChar) - { - var argument = arguments[0]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance!, argument); - - return _sqlExpressionFactory.Subtract( - _sqlExpressionFactory.Function( - "strpos", - [ - _sqlExpressionFactory.ApplyTypeMapping(instance!, stringTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(argument, stringTypeMapping) - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - method.ReturnType), - _sqlExpressionFactory.Constant(1)); - } - - if (method == Replace) - { - var oldValue = arguments[0]; - var newValue = arguments[1]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance!, oldValue, newValue); - - return _sqlExpressionFactory.Function( - "replace", - [ - _sqlExpressionFactory.ApplyTypeMapping(instance!, stringTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(oldValue, stringTypeMapping), - _sqlExpressionFactory.ApplyTypeMapping(newValue, stringTypeMapping) - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - method.ReturnType, - stringTypeMapping); - } - - if (method == ToLower || method == ToUpper) - { - return _sqlExpressionFactory.Function( - method == ToLower ? "lower" : "upper", - [instance!], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - method.ReturnType, - instance!.TypeMapping); - } - - if (method == Substring || method == SubstringWithLength) - { - var args = - method == Substring - ? [instance!, GenerateOneBasedIndexExpression(arguments[0])] - : new[] { instance!, GenerateOneBasedIndexExpression(arguments[0]), arguments[1] }; - return _sqlExpressionFactory.Function( - "substring", - args, - nullable: true, - argumentsPropagateNullability: TrueArrays[args.Length], - method.ReturnType, - instance!.TypeMapping); - } - - if (method == IsNullOrWhiteSpace) - { - var argument = arguments[0]; - - return _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.IsNull(argument), - _sqlExpressionFactory.Equal( - _sqlExpressionFactory.Function( - "btrim", - [argument, _whitespace], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - argument.Type, - argument.TypeMapping), - _sqlExpressionFactory.Constant(string.Empty, argument.TypeMapping))); - } - - var isTrimStart = method == TrimStartWithNoParam || method == TrimStartWithChars || method == TrimStartWithSingleChar; - var isTrimEnd = method == TrimEndWithNoParam || method == TrimEndWithChars || method == TrimEndWithSingleChar; - var isTrimBoth = method == TrimBothWithNoParam || method == TrimBothWithChars || method == TrimBothWithSingleChar; - if (isTrimStart || isTrimEnd || isTrimBoth) - { - char[]? trimChars = null; - - if (method == TrimStartWithChars - || method == TrimStartWithSingleChar - || method == TrimEndWithChars - || method == TrimEndWithSingleChar - || method == TrimBothWithChars - || method == TrimBothWithSingleChar) - { - var constantTrimChars = arguments[0] as SqlConstantExpression; - if (constantTrimChars is null) - { - return null; // Don't translate if trim chars isn't a constant - } - - trimChars = constantTrimChars.Value is char c - ? [c] - : (char[]?)constantTrimChars.Value; - } - - return _sqlExpressionFactory.Function( - isTrimStart ? "ltrim" : isTrimEnd ? "rtrim" : "btrim", - [ - instance!, - trimChars is null || trimChars.Length == 0 - ? _whitespace - : _sqlExpressionFactory.Constant(new string(trimChars)) - ], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - instance!.Type, - instance.TypeMapping); - } - - if (method == PadLeft || method == PadLeftWithChar || method == PadRight || method == PadRightWithChar) - { - var args = - method == PadLeft || method == PadRight - ? [instance!, arguments[0]] - : new[] { instance!, arguments[0], arguments[1] }; - - return _sqlExpressionFactory.Function( - method == PadLeft || method == PadLeftWithChar ? "lpad" : "rpad", - args, - nullable: true, - argumentsPropagateNullability: TrueArrays[args.Length], - instance!.Type, - instance.TypeMapping); - } - - if (method.DeclaringType == typeof(string) - && (method == String_Join1 - || method == String_Join2 - || method == String_Join3 - || method == String_Join4 - || method == String_Join5 - || method.IsClosedFormOf(String_Join_generic1) - || 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. - if (arguments[1] is PgNewArrayExpression newArrayExpression) - { - var rewrittenArguments = new SqlExpression[newArrayExpression.Expressions.Count + 1]; - rewrittenArguments[0] = arguments[0]; - - for (var i = 0; i < newArrayExpression.Expressions.Count; i++) - { - var argument = newArrayExpression.Expressions[i]; - - rewrittenArguments[i + 1] = argument switch - { - ColumnExpression { IsNullable: false } => argument, - SqlConstantExpression constantExpression => constantExpression.Value is null - ? _sqlExpressionFactory.Constant(string.Empty, typeof(string)) - : constantExpression, - _ => _sqlExpressionFactory.Coalesce(argument, _sqlExpressionFactory.Constant(string.Empty, typeof(string))) - }; - } - - // Only the delimiter (first arg) propagates nullability - all others are non-nullable, since we wrap the others in coalesce - // (where needed). - var argumentsPropagateNullability = new bool[rewrittenArguments.Length]; - argumentsPropagateNullability[0] = true; - - return _sqlExpressionFactory.Function( - "concat_ws", - rewrittenArguments, - nullable: true, - argumentsPropagateNullability, - typeof(string)); - } - - return _sqlExpressionFactory.Function( - "array_to_string", - [arguments[1], arguments[0], _sqlExpressionFactory.Constant("")], - nullable: true, - argumentsPropagateNullability: TrueArrays[3], - typeof(string)); - } - - return null; - } - - private SqlExpression? TranslateDbFunctionsMethod(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments) - { - if (method == Reverse) - { - return _sqlExpressionFactory.Function( - "reverse", - [arguments[1]], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(string), - arguments[1].TypeMapping); - } - - if (method == StringToArray) - { - // Note that string_to_array always returns text[], regardless of the input type - return _sqlExpressionFactory.Function( - "string_to_array", - [arguments[1], arguments[2]], - nullable: true, - argumentsPropagateNullability: [true, false], - typeof(string[]), - _typeMappingSource.FindMapping(typeof(string[]))); - } - - if (method == StringToArrayNullString) - { - // Note that string_to_array always returns text[], regardless of the input type - return _sqlExpressionFactory.Function( - "string_to_array", - [arguments[1], arguments[2], arguments[3]], - nullable: true, - 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; - } - - private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) - => expression is SqlConstantExpression constant - ? _sqlExpressionFactory.Constant(Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) - : _sqlExpressionFactory.Add(expression, _sqlExpressionFactory.Constant(1)); -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs deleted file mode 100644 index a191699f2e..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs +++ /dev/null @@ -1,87 +0,0 @@ -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlTimeSpanMemberTranslator : IMemberTranslator -{ - 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 NpgsqlTimeSpanMemberTranslator(ISqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - } - - private static readonly bool[] FalseTrueArray = [false, 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. - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MemberInfo member, - Type returnType, - IDiagnosticsLogger logger) - { - Check.NotNull(member, nameof(member)); - Check.NotNull(returnType, nameof(returnType)); - - if (member.DeclaringType == typeof(TimeSpan) && instance is not null) - { - return member.Name switch - { - nameof(TimeSpan.Days) => Floor(DatePart("day", instance)), - nameof(TimeSpan.Hours) => Floor(DatePart("hour", instance)), - nameof(TimeSpan.Minutes) => Floor(DatePart("minute", instance)), - nameof(TimeSpan.Seconds) => Floor(DatePart("second", instance)), - nameof(TimeSpan.Milliseconds) => _sqlExpressionFactory.Modulo( - Floor(DatePart("millisecond", instance)), - _sqlExpressionFactory.Constant(1000)), - - nameof(TimeSpan.TotalDays) => TranslateDurationTotalMember(instance, 86400), - nameof(TimeSpan.TotalHours) => TranslateDurationTotalMember(instance, 3600), - nameof(TimeSpan.TotalMinutes) => TranslateDurationTotalMember(instance, 60), - nameof(TimeSpan.TotalSeconds) => DatePart("epoch", instance), - nameof(TimeSpan.TotalMilliseconds) => TranslateDurationTotalMember(instance, 0.001), - - _ => null - }; - } - - return null; - - SqlExpression Floor(SqlExpression value) - => _sqlExpressionFactory.Convert( - _sqlExpressionFactory.Function( - "floor", - [value], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(double)), - typeof(int)); - - SqlExpression DatePart(string part, SqlExpression value) - => _sqlExpressionFactory.Function( - "date_part", [_sqlExpressionFactory.Constant(part), value], - nullable: true, - argumentsPropagateNullability: FalseTrueArray, - returnType); - - 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 deleted file mode 100644 index 291dcd3d48..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTrigramsMethodTranslator.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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 NpgsqlTrigramsMethodTranslator : IMethodCallTranslator -{ - private static readonly Dictionary Functions = new() - { - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsShow), typeof(DbFunctions), typeof(string))] - = "show_trgm", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsSimilarity), typeof(DbFunctions), typeof(string), typeof(string))] - = "similarity", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsWordSimilarity), typeof(DbFunctions), typeof(string), typeof(string))] - = "word_similarity", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsStrictWordSimilarity), typeof(DbFunctions), typeof(string), typeof(string))] - = "strict_word_similarity" - }; - - private static readonly Dictionary BoolReturningOperators = new() - { - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsAreSimilar), typeof(DbFunctions), typeof(string), typeof(string))] - = "%", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsAreWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] - = "<%", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsAreNotWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] - = "%>", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsAreStrictWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] - = "<<%", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsAreNotStrictWordSimilar), typeof(DbFunctions), typeof(string), typeof(string))] - = "%>>" - }; - - private static readonly Dictionary FloatReturningOperators = new() - { - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsSimilarityDistance), typeof(DbFunctions), typeof(string), typeof(string))] - = "<->", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsWordSimilarityDistance), typeof(DbFunctions), typeof(string), typeof(string))] - = "<<->", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsWordSimilarityDistanceInverted), typeof(DbFunctions), typeof(string), typeof(string))] - = "<->>", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsStrictWordSimilarityDistance), typeof(DbFunctions), typeof(string), typeof(string))] - = "<<<->", - [GetRuntimeMethod(nameof(NpgsqlTrigramsDbFunctionsExtensions.TrigramsStrictWordSimilarityDistanceInverted), typeof(DbFunctions), typeof(string), typeof(string))] - = "<->>>" - }; - - private static MethodInfo GetRuntimeMethod(string name, params Type[] parameters) - => typeof(NpgsqlTrigramsDbFunctionsExtensions).GetRuntimeMethod(name, parameters)!; - - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly RelationalTypeMapping _boolMapping; - private readonly RelationalTypeMapping _floatMapping; - - private static readonly bool[][] TrueArrays = [[], [true], [true, 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. - /// - public NpgsqlTrigramsMethodTranslator( - IRelationalTypeMappingSource typeMappingSource, - NpgsqlSqlExpressionFactory sqlExpressionFactory, - IModel model) - { - _sqlExpressionFactory = sqlExpressionFactory; - _boolMapping = typeMappingSource.FindMapping(typeof(bool), model)!; - _floatMapping = typeMappingSource.FindMapping(typeof(float), model)!; - } - -#pragma warning disable EF1001 - /// - public virtual SqlExpression? Translate( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (Functions.TryGetValue(method, out var function)) - { - return _sqlExpressionFactory.Function( - function, - arguments.Skip(1), - nullable: true, - argumentsPropagateNullability: TrueArrays[arguments.Count - 1], - method.ReturnType); - } - - if (BoolReturningOperators.TryGetValue(method, out var boolOperator)) - { - return new PgUnknownBinaryExpression( - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - boolOperator, - _boolMapping.ClrType, - _boolMapping); - } - - if (FloatReturningOperators.TryGetValue(method, out var floatOperator)) - { - return new PgUnknownBinaryExpression( - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), - _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[2]), - floatOperator, - _floatMapping.ClrType, - _floatMapping); - } - - return null; - } -#pragma warning restore EF1001 -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs deleted file mode 100644 index 0ee27abd38..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAllExpression.cs +++ /dev/null @@ -1,137 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// Represents a PostgreSQL array ALL expression. -/// -/// -/// See https://www.postgresql.org/docs/current/static/functions-comparisons.html -/// -public class PgAllExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - public override Type Type - => typeof(bool); - - /// - /// The value to test against the . - /// - public virtual SqlExpression Item { get; } - - /// - /// The array of values or patterns to test for the . - /// - public virtual SqlExpression Array { get; } - - /// - /// The operator. - /// - public virtual PgAllOperatorType OperatorType { get; } - - /// - /// Constructs a . - /// - /// The operator symbol to the array expression. - /// The value to find. - /// The array to search. - /// The type mapping for the expression. - public PgAllExpression( - SqlExpression item, - SqlExpression array, - PgAllOperatorType operatorType, - RelationalTypeMapping? typeMapping) - : base(typeof(bool), typeMapping) - { - if (array.Type.TryGetElementType(typeof(IEnumerable<>)) is null) - { - throw new ArgumentException("Array expression must be an IEnumerable", nameof(array)); - } - - Item = item; - Array = array; - OperatorType = operatorType; - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - public virtual PgAllExpression Update(SqlExpression item, SqlExpression array) - => item != Item || array != 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)); - - /// - public override bool Equals(object? obj) - => obj is PgAllExpression e && Equals(e); - - /// - public virtual bool Equals(PgAllExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && Item.Equals(other.Item) - && Array.Equals(other.Array) - && OperatorType.Equals(other.OperatorType); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Item, Array, OperatorType); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Item); - expressionPrinter - .Append(" ") - .Append( - OperatorType switch - { - PgAllOperatorType.Like => "LIKE", - PgAllOperatorType.ILike => "ILIKE", - - _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") - }) - .Append(" ALL("); - expressionPrinter.Visit(Array); - expressionPrinter.Append(")"); - } - - /// - public override string ToString() - => $"{Item} {OperatorType} ALL({Array})"; -} - -/// -/// Determines the operator type for a . -/// -public enum PgAllOperatorType -{ - /// - /// Represents a PostgreSQL LIKE ALL operator. - /// - Like, - - /// - /// Represents a PostgreSQL ILIKE ALL operator. - /// - ILike, -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs deleted file mode 100644 index 40c7326449..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgAnyExpression.cs +++ /dev/null @@ -1,154 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// Represents a PostgreSQL array ANY expression. -/// -/// -/// 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}') -/// -/// -/// See https://www.postgresql.org/docs/current/static/functions-comparisons.html -/// -public class PgAnyExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - public override Type Type - => typeof(bool); - - /// - /// The value to test against the . - /// - public virtual SqlExpression Item { get; } - - /// - /// The array of values or patterns to test for the . - /// - public virtual SqlExpression Array { get; } - - /// - /// The operator. - /// - public virtual PgAnyOperatorType OperatorType { get; } - - /// - /// Constructs a . - /// - /// The operator symbol to the array expression. - /// The value to find. - /// The array to search. - /// The type mapping for the expression. - public PgAnyExpression( - SqlExpression item, - SqlExpression array, - PgAnyOperatorType operatorType, - RelationalTypeMapping? typeMapping) - : base(typeof(bool), typeMapping) - { - if (array is not SqlConstantExpression { Value: null }) - { - if (array.Type.TryGetElementType(typeof(IEnumerable<>)) is null) - { - throw new ArgumentException("Array expression must be an IEnumerable", nameof(array)); - } - - if (array is SqlConstantExpression && operatorType == PgAnyOperatorType.Equal) - { - throw new ArgumentException($"Use {nameof(InExpression)} for equality against constant arrays", nameof(array)); - } - } - - Item = item; - Array = array; - OperatorType = operatorType; - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - public virtual PgAnyExpression Update(SqlExpression item, SqlExpression array) - => item != Item || array != 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)); - - /// - public override bool Equals(object? obj) - => obj is PgAnyExpression e && Equals(e); - - /// - public virtual bool Equals(PgAnyExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && Item.Equals(other.Item) - && Array.Equals(other.Array) - && OperatorType.Equals(other.OperatorType); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Item, Array, OperatorType); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Item); - expressionPrinter - .Append(" ") - .Append( - OperatorType switch - { - PgAnyOperatorType.Equal => "=", - PgAnyOperatorType.Like => "LIKE", - PgAnyOperatorType.ILike => "ILIKE", - - _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") - }) - .Append(" ANY("); - expressionPrinter.Visit(Array); - expressionPrinter.Append(")"); - } - - /// - public override string ToString() - => $"{Item} {OperatorType} ANY({Array})"; -} - -/// -/// Determines the operator type for a . -/// -public enum PgAnyOperatorType -{ - /// - /// Represents a PostgreSQL = ANY operator. - /// - Equal, - - /// - /// Represents a PostgreSQL LIKE ANY operator. - /// - Like, - - /// - /// Represents a PostgreSQL ILIKE ANY operator. - /// - ILike, -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs deleted file mode 100644 index d3e676fb7b..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgArrayIndexExpression.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// An SQL expression that represents an indexing into a PostgreSQL array. -/// -/// -/// specifically disallows having an -/// of value as arrays are a PostgreSQL-only feature. -/// -public class PgArrayIndexExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// The array being indexed. - /// - public virtual SqlExpression Array { get; } - - /// - /// The index in the array. - /// - public virtual SqlExpression Index { get; } - - /// - /// Whether the expression is nullable. - /// - public virtual bool IsNullable { get; } - - /// - /// Creates a new instance of the class. - /// - /// The array tp index into. - /// An position in the array to index into. - /// Whether the expression is nullable. - /// The of the expression. - /// The associated with the expression. - public PgArrayIndexExpression( - SqlExpression array, - SqlExpression index, - bool nullable, - Type type, - RelationalTypeMapping? typeMapping) - : base(type.UnwrapNullableType(), typeMapping) - { - Check.NotNull(array, nameof(array)); - Check.NotNull(index, nameof(index)); - - if (!array.Type.TryGetElementType(out var elementType)) - { - throw new ArgumentException("Array expression must of an array type", nameof(array)); - } - - if (type.UnwrapNullableType() != elementType.UnwrapNullableType()) - { - throw new ArgumentException($"Mismatch between array type ({array.Type.Name}) and expression type ({type})"); - } - - if (index.Type != typeof(int)) - { - throw new ArgumentException("Index expression must of type int", nameof(index)); - } - - Array = array; - Index = index; - IsNullable = nullable; - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - public virtual PgArrayIndexExpression Update(SqlExpression array, SqlExpression index) - => array == Array && index == Index - ? 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)); - - /// - public virtual bool Equals(PgArrayIndexExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && Array.Equals(other.Array) - && Index.Equals(other.Index) - && IsNullable == other.IsNullable; - - /// - public override bool Equals(object? obj) - => obj is PgArrayIndexExpression e && Equals(e); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Array, Index, IsNullable); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Array); - expressionPrinter.Append("["); - expressionPrinter.Visit(Index); - expressionPrinter.Append("]"); - } - - /// - public override string ToString() - => $"{Array}[{Index}]"; -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs deleted file mode 100644 index c8a7494028..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgArraySliceExpression.cs +++ /dev/null @@ -1,128 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// A SQL expression that represents a slicing into a PostgreSQL array (e.g. array[2:3]). -/// -/// -/// . -/// -public class PgArraySliceExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// The array being sliced. - /// - public virtual SqlExpression Array { get; } - - /// - /// The lower bound of the slice. - /// - public virtual SqlExpression? LowerBound { get; } - - /// - /// The upper bound of the slice. - /// - public virtual SqlExpression? UpperBound { get; } - - /// - /// Whether the expression is nullable. - /// - public virtual bool IsNullable { get; } - - /// - /// Creates a new instance of the class. - /// - /// The array tp slice into. - /// The lower bound of the slice. - /// The upper bound of the slice. - /// Whether the expression is nullable. - /// The of the expression. - /// The associated with the expression. - public PgArraySliceExpression( - SqlExpression array, - SqlExpression? lowerBound, - SqlExpression? upperBound, - bool nullable, - Type type, - RelationalTypeMapping? typeMapping) - : base(type.UnwrapNullableType(), typeMapping) - { - Check.NotNull(array, nameof(array)); - - if (lowerBound is null && upperBound is null) - { - throw new ArgumentException("At least one of lowerBound or upperBound must be provided"); - } - - Array = array; - LowerBound = lowerBound; - UpperBound = upperBound; - IsNullable = nullable; - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// The lower bound of the slice. - /// The upper bound of the slice. - /// This expression if no children changed, or an expression with the updated children. - public virtual PgArraySliceExpression Update(SqlExpression array, SqlExpression? lowerBound, SqlExpression? upperBound) - => array == Array && lowerBound == LowerBound && upperBound == UpperBound - ? 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( - (SqlExpression)visitor.Visit(Array), - (SqlExpression?)visitor.Visit(LowerBound), - (SqlExpression?)visitor.Visit(UpperBound)); - - /// - public virtual bool Equals(PgArraySliceExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && Array.Equals(other.Array) - && (LowerBound is null ? other.LowerBound is null : LowerBound.Equals(other.LowerBound)) - && (UpperBound is null ? other.UpperBound is null : UpperBound.Equals(other.UpperBound)) - && IsNullable == other.IsNullable; - - /// - public override bool Equals(object? obj) - => obj is PgArraySliceExpression e && Equals(e); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Array, LowerBound, UpperBound); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Array); - expressionPrinter.Append("["); - expressionPrinter.Visit(LowerBound); - expressionPrinter.Append(":"); - expressionPrinter.Visit(UpperBound); - expressionPrinter.Append("]"); - } - - /// - public override string ToString() - => $"{Array}[{LowerBound}:{UpperBound}]"; -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs deleted file mode 100644 index 03387b9887..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs +++ /dev/null @@ -1,192 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// An expression that represents a PostgreSQL-specific binary operation in a SQL tree. -/// -public class PgBinaryExpression : SqlExpression -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// Creates a new instance of the class. - /// - /// The operator to apply. - /// An expression which is left operand. - /// An expression which is right operand. - /// The of the expression. - /// The associated with the expression. - public PgBinaryExpression( - PgExpressionType operatorType, - SqlExpression left, - SqlExpression right, - Type type, - RelationalTypeMapping? typeMapping) - : base(type, typeMapping) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - OperatorType = operatorType; - Left = left; - Right = right; - } - - /// - /// The operator of this PostgreSQL binary operation. - /// - public virtual PgExpressionType OperatorType { get; } - - /// - /// The left operand. - /// - public virtual SqlExpression Left { get; } - - /// - /// The right operand. - /// - public virtual SqlExpression Right { get; } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - var left = (SqlExpression)visitor.Visit(Left); - var right = (SqlExpression)visitor.Visit(Right); - - return Update(left, right); - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - public virtual PgBinaryExpression Update(SqlExpression left, SqlExpression right) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - return left != Left || right != Right - ? new PgBinaryExpression(OperatorType, left, right, Type, TypeMapping) - : 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) - { - Check.NotNull(expressionPrinter, nameof(expressionPrinter)); - - var requiresBrackets = RequiresBrackets(Left); - - if (requiresBrackets) - { - expressionPrinter.Append("("); - } - - expressionPrinter.Visit(Left); - - if (requiresBrackets) - { - expressionPrinter.Append(")"); - } - - expressionPrinter - .Append(" ") - .Append( - OperatorType switch - { - PgExpressionType.Contains => "@>", - PgExpressionType.ContainedBy => "<@", - PgExpressionType.Overlaps => "&&", - - PgExpressionType.NetworkContainedByOrEqual => "<<=", - PgExpressionType.NetworkContainsOrEqual => ">>=", - PgExpressionType.NetworkContainsOrContainedBy => "&&", - - PgExpressionType.RangeIsStrictlyLeftOf => "<<", - PgExpressionType.RangeIsStrictlyRightOf => ">>", - PgExpressionType.RangeDoesNotExtendRightOf => "&<", - PgExpressionType.RangeDoesNotExtendLeftOf => "&>", - PgExpressionType.RangeIsAdjacentTo => "-|-", - PgExpressionType.RangeUnion => "+", - PgExpressionType.RangeIntersect => "*", - PgExpressionType.RangeExcept => "-", - - PgExpressionType.TextSearchMatch => "@@", - PgExpressionType.TextSearchAnd => "&&", - PgExpressionType.TextSearchOr => "||", - - PgExpressionType.JsonExists => "?", - PgExpressionType.JsonExistsAny => "?|", - PgExpressionType.JsonExistsAll => "?&", - - PgExpressionType.LTreeMatches - when Right.TypeMapping is { StoreType: "lquery" } or NpgsqlArrayTypeMapping - { - ElementTypeMapping.StoreType: "lquery" - } - => "~", - PgExpressionType.LTreeMatches when Right.TypeMapping?.StoreType == "ltxtquery" => "@", - PgExpressionType.LTreeMatchesAny => "?", - PgExpressionType.LTreeFirstAncestor => "?@>", - PgExpressionType.LTreeFirstDescendent => "?<@", - PgExpressionType.LTreeFirstMatches when Right.TypeMapping?.StoreType == "lquery" => "?~", - PgExpressionType.LTreeFirstMatches when Right.TypeMapping?.StoreType == "ltxtquery" => "?@", - - PgExpressionType.Distance => "<->", - - _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") - }) - .Append(" "); - - requiresBrackets = RequiresBrackets(Right); - - if (requiresBrackets) - { - expressionPrinter.Append("("); - } - - expressionPrinter.Visit(Right); - - if (requiresBrackets) - { - expressionPrinter.Append(")"); - } - - static bool RequiresBrackets(SqlExpression expression) - => expression is PgBinaryExpression or LikeExpression; - } - - /// - public override bool Equals(object? obj) - => obj is not null - && (ReferenceEquals(this, obj) - || obj is PgBinaryExpression sqlBinaryExpression - && Equals(sqlBinaryExpression)); - - private bool Equals(PgBinaryExpression sqlBinaryExpression) - => base.Equals(sqlBinaryExpression) - && OperatorType == sqlBinaryExpression.OperatorType - && Left.Equals(sqlBinaryExpression.Left) - && Right.Equals(sqlBinaryExpression.Right); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), OperatorType, Left, Right); -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgDeleteExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgDeleteExpression.cs deleted file mode 100644 index dee94c3368..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgDeleteExpression.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// An SQL expression that represents a PostgreSQL DELETE operation. -/// -public sealed class PgDeleteExpression : Expression, IPrintableExpression -{ - /// - /// The tables that rows are to be deleted from. - /// - public TableExpression Table { get; } - - /// - /// Additional tables which can be referenced in the predicate. - /// - public IReadOnlyList FromItems { get; } - - /// - /// The WHERE predicate for the DELETE. - /// - public SqlExpression? Predicate { get; } - - /// - /// The list of tags applied to this . - /// - public ISet Tags { get; } - - /// - /// Creates a new instance of the class. - /// - public PgDeleteExpression( - TableExpression table, - IReadOnlyList fromItems, - SqlExpression? predicate, - ISet tags) - { - (Table, FromItems, Predicate, Tags) = (table, fromItems, predicate, tags); - } - - /// - public override Type Type - => typeof(object); - - /// - public override ExpressionType NodeType - => ExpressionType.Extension; - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Predicate is null - ? this - : Update((SqlExpression?)visitor.Visit(Predicate)); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - public PgDeleteExpression Update(SqlExpression? predicate) - => predicate == Predicate - ? this - : new PgDeleteExpression(Table, FromItems, predicate, Tags); - - /// - public void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.AppendLine($"DELETE FROM {Table.Name} AS {Table.Alias}"); - - if (FromItems.Count > 0) - { - var first = true; - foreach (var fromItem in FromItems) - { - if (first) - { - expressionPrinter.Append("USING "); - first = false; - } - else - { - expressionPrinter.Append(", "); - } - - expressionPrinter.Visit(fromItem); - } - } - - if (Predicate is not null) - { - expressionPrinter.Append("WHERE "); - expressionPrinter.Visit(Predicate); - } - } - - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is PgDeleteExpression pgDeleteExpression - && Equals(pgDeleteExpression)); - - private bool Equals(PgDeleteExpression pgDeleteExpression) - => Table == pgDeleteExpression.Table - && FromItems.SequenceEqual(pgDeleteExpression.FromItems) - && (Predicate is null ? pgDeleteExpression.Predicate is null : Predicate.Equals(pgDeleteExpression.Predicate)); - - /// - public override int GetHashCode() - => Table.GetHashCode(); -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs deleted file mode 100644 index 8b98e44e92..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs +++ /dev/null @@ -1,326 +0,0 @@ -#pragma warning disable 8632 - -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)). -/// -public class PgFunctionExpression : SqlFunctionExpression, IEquatable -{ - /// - public new virtual IReadOnlyList Arguments - => base.Arguments!; - - /// - public new virtual IReadOnlyList ArgumentsPropagateNullability - => base.ArgumentsPropagateNullability!; - - /// - /// For aggregate methods, contains whether to apply distinct. - /// - public virtual bool IsAggregateDistinct { get; } - - /// - /// For aggregate methods, contains the predicate to be applied (generated as the SQL FILTER clause). - /// - public virtual SqlExpression? AggregatePredicate { get; } - - /// - /// For aggregate methods, contains the orderings to be applied. - /// - public virtual IReadOnlyList AggregateOrderings { get; } - - /// - /// List of argument names, corresponding position-wise to arguments in . - /// Unnamed (positional) arguments must come first, so this list must contain possible nulls, followed by - /// non-nulls. - /// - public virtual IReadOnlyList ArgumentNames { get; } - - /// - /// List of non-comma separators between argument separators, in the order in which they appear between - /// the arguments. null as well as positions beyond the end of the list mean regular commas. - /// - public virtual IReadOnlyList ArgumentSeparators { get; } - - /// - /// Creates an instance of with named arguments. - /// - public static PgFunctionExpression CreateWithNamedArguments( - string name, - IEnumerable arguments, - IEnumerable argumentNames, - bool nullable, - IEnumerable argumentsPropagateNullability, - bool builtIn, - Type type, - RelationalTypeMapping? typeMapping) - { - Check.NotNull(arguments, nameof(arguments)); - Check.NotNull(argumentNames, nameof(argumentNames)); - - return new PgFunctionExpression( - name, arguments, argumentNames, argumentSeparators: null, - aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: [], - nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping); - } - - /// - /// Creates an instance of with argument separators. - /// - public static PgFunctionExpression CreateWithArgumentSeparators( - string name, - IEnumerable arguments, - IEnumerable argumentSeparators, - bool nullable, - IEnumerable argumentsPropagateNullability, - bool builtIn, - Type type, - RelationalTypeMapping? typeMapping) - { - Check.NotNull(arguments, nameof(arguments)); - Check.NotNull(argumentSeparators, nameof(argumentSeparators)); - - return new PgFunctionExpression( - name, arguments, argumentNames: null, argumentSeparators: argumentSeparators, - aggregateDistinct: false, aggregatePredicate: null, aggregateOrderings: [], - nullable: nullable, argumentsPropagateNullability: argumentsPropagateNullability, type: type, typeMapping: typeMapping); - } - - /// - /// Creates a new instance of . - /// - public PgFunctionExpression( - string name, - IEnumerable arguments, - IEnumerable? argumentNames, - IEnumerable? argumentSeparators, - bool aggregateDistinct, - SqlExpression? aggregatePredicate, - IReadOnlyList aggregateOrderings, - bool nullable, - IEnumerable argumentsPropagateNullability, - Type type, - RelationalTypeMapping? typeMapping) - : base(name, arguments, nullable, argumentsPropagateNullability, type, typeMapping) - { - Check.NotEmpty(name, nameof(name)); - Check.NotNull(type, nameof(type)); - - ArgumentNames = (argumentNames ?? []).ToList(); - ArgumentSeparators = (argumentSeparators ?? []).ToList(); - - if (ArgumentNames.SkipWhile(a => a is null).Contains(null)) - { - throw new ArgumentException($"{nameof(argumentNames)} must contain nulls followed by non-nulls", nameof(argumentNames)); - } - - IsAggregateDistinct = aggregateDistinct; - AggregatePredicate = aggregatePredicate; - AggregateOrderings = aggregateOrderings; - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - var changed = false; - - // Note that we don't have instance functions in PG - - SqlExpression[]? visitedArguments = null; - - if (!IsNiladic) - { - for (var i = 0; i < Arguments.Count; i++) - { - var visitedArgument = (SqlExpression)visitor.Visit(Arguments[i]); - - if (visitedArgument != Arguments[i] && visitedArguments is null) - { - changed = true; - visitedArguments = new SqlExpression[Arguments.Count]; - - for (var j = 0; j < visitedArguments.Length; j++) - { - visitedArguments[j] = Arguments[j]; - } - } - - if (visitedArguments is not null) - { - visitedArguments[i] = visitedArgument; - } - } - } - - var visitedAggregatePredicate = (SqlExpression?)visitor.Visit(AggregatePredicate); - changed |= visitedAggregatePredicate != AggregatePredicate; - - OrderingExpression[]? visitedAggregateOrderings = null; - - for (var i = 0; i < AggregateOrderings.Count; i++) - { - var visitedOrdering = (OrderingExpression)visitor.Visit(AggregateOrderings[i]); - if (visitedOrdering != AggregateOrderings[i] && visitedAggregateOrderings is null) - { - changed = true; - visitedAggregateOrderings = new OrderingExpression[AggregateOrderings.Count]; - - for (var j = 0; j < visitedAggregateOrderings.Length; j++) - { - visitedAggregateOrderings[j] = AggregateOrderings[j]; - } - } - - if (visitedAggregateOrderings is not null) - { - visitedAggregateOrderings[i] = visitedOrdering; - } - } - - return changed - ? new PgFunctionExpression( - Name, visitedArguments ?? Arguments, ArgumentNames, ArgumentSeparators, - IsAggregateDistinct, - visitedAggregatePredicate ?? AggregatePredicate, - visitedAggregateOrderings ?? AggregateOrderings, - IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping) - : this; - } - - /// - public override SqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) - => new PgFunctionExpression( - Name, - Arguments, - ArgumentNames, - ArgumentSeparators, - IsAggregateDistinct, - AggregatePredicate, - AggregateOrderings, - IsNullable, - ArgumentsPropagateNullability, Type, typeMapping ?? TypeMapping); - - /// - public override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList? arguments) - { - Check.NotNull(arguments, nameof(arguments)); - - if (instance is not null) - { - throw new ArgumentException("Must be null", nameof(instance)); - } - - return !arguments.SequenceEqual(Arguments) - ? new PgFunctionExpression( - Name, arguments, ArgumentNames, ArgumentSeparators, - IsAggregateDistinct, - AggregatePredicate, - AggregateOrderings, - IsNullable, ArgumentsPropagateNullability, Type, TypeMapping) - : this; - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - public virtual PgFunctionExpression UpdateAggregateComponents( - SqlExpression? predicate, - IReadOnlyList orderings) - => predicate != AggregatePredicate || orderings != AggregateOrderings - ? new PgFunctionExpression( - Name, Arguments, ArgumentNames, ArgumentSeparators, - IsAggregateDistinct, - predicate, - orderings, - IsNullable, ArgumentsPropagateNullability, Type, TypeMapping) - : this; - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - if (!string.IsNullOrEmpty(Schema)) - { - expressionPrinter.Append(Schema).Append(".").Append(Name); - } - else - { - Check.DebugAssert(Instance is null, "Instance is null"); - - expressionPrinter.Append(Name); - } - - if (!IsNiladic) - { - expressionPrinter.Append("("); - - if (IsAggregateDistinct) - { - expressionPrinter.Append("DISTINCT "); - } - - expressionPrinter.VisitCollection(Arguments); - - if (AggregateOrderings.Count > 0) - { - expressionPrinter.Append(" ORDER BY "); - expressionPrinter.VisitCollection(AggregateOrderings); - } - - expressionPrinter.Append(")"); - - if (AggregatePredicate is not null) - { - expressionPrinter.Append(" FILTER (WHERE "); - expressionPrinter.Visit(AggregatePredicate); - expressionPrinter.Append(")"); - } - } - } - - /// - public override bool Equals(object? obj) - => obj is PgFunctionExpression pgFunction && Equals(pgFunction); - - /// - public virtual bool Equals(PgFunctionExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && ArgumentNames.SequenceEqual(other.ArgumentNames) - && ArgumentSeparators.SequenceEqual(other.ArgumentSeparators) - && AggregateOrderings.SequenceEqual(other.AggregateOrderings) - && (AggregatePredicate is null && other.AggregatePredicate is null - || AggregatePredicate != null && AggregatePredicate.Equals(other.AggregatePredicate)); - - /// - public override int GetHashCode() - { - var hash = new HashCode(); - - hash.Add(base.GetHashCode()); - - foreach (var argumentName in ArgumentNames) - { - hash.Add(argumentName); - } - - foreach (var argumentSeparator in ArgumentSeparators) - { - hash.Add(argumentSeparator); - } - - foreach (var aggregateOrdering in AggregateOrderings) - { - hash.Add(aggregateOrdering); - } - - hash.Add(AggregatePredicate); - - return hash.ToHashCode(); - } -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs deleted file mode 100644 index 9b816886d5..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgILikeExpression.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// Represents a PostgreSQL ILIKE expression. -/// -// ReSharper disable once InconsistentNaming -public class PgILikeExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// The match expression. - /// - public virtual SqlExpression Match { get; } - - /// - /// The pattern to match. - /// - public virtual SqlExpression Pattern { get; } - - /// - /// The escape character to use in . - /// - public virtual SqlExpression? EscapeChar { get; } - - /// - /// Constructs a . - /// - /// The expression to match. - /// The pattern to match. - /// The escape character to use in . - /// The associated with the expression. - /// - public PgILikeExpression( - SqlExpression match, - SqlExpression pattern, - SqlExpression? escapeChar, - RelationalTypeMapping? typeMapping) - : base(typeof(bool), typeMapping) - { - Match = match; - Pattern = pattern; - EscapeChar = escapeChar; - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update( - (SqlExpression)visitor.Visit(Match), - (SqlExpression)visitor.Visit(Pattern), - EscapeChar is null ? null : (SqlExpression)visitor.Visit(EscapeChar)); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - public virtual PgILikeExpression Update( - SqlExpression match, - SqlExpression pattern, - SqlExpression? escapeChar) - => match == Match && pattern == Pattern && escapeChar == EscapeChar - ? 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); - - /// - public virtual bool Equals(PgILikeExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && Equals(Match, other.Match) - && Equals(Pattern, other.Pattern) - && Equals(EscapeChar, other.EscapeChar); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Match, Pattern, EscapeChar); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Match); - expressionPrinter.Append(" ILIKE "); - expressionPrinter.Visit(Pattern); - - if (EscapeChar is not null) - { - expressionPrinter.Append(" ESCAPE "); - expressionPrinter.Visit(EscapeChar); - } - } - - /// - public override string ToString() - => $"{Match} ILIKE {Pattern}{(EscapeChar is null ? "" : $" ESCAPE {EscapeChar}")}"; -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs deleted file mode 100644 index 61871b5673..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgJsonTraversalExpression.cs +++ /dev/null @@ -1,125 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// Represents a PostgreSQL JSON operator traversing a JSON document with a path (i.e. x#>y or x#>>y) -/// -public class PgJsonTraversalExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// The match expression. - /// - public virtual SqlExpression Expression { get; } - - /// - /// The pattern to match. - /// - public virtual IReadOnlyList Path { get; } - - /// - /// Whether the text-returning operator (x#>>y) or the object-returning operator (x#>y) is used. - /// - public virtual bool ReturnsText { get; } - - /// - /// Constructs a . - /// - public PgJsonTraversalExpression( - SqlExpression expression, - IReadOnlyList path, - bool returnsText, - Type type, - RelationalTypeMapping? typeMapping) - : base(type, typeMapping) - { - if (returnsText && type != typeof(string)) - { - throw new ArgumentException($"{nameof(type)} must be string", nameof(type)); - } - - Expression = expression; - Path = path; - ReturnsText = returnsText; - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update( - (SqlExpression)visitor.Visit(Expression), - Path.Select(p => (SqlExpression)visitor.Visit(p)).ToArray()); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - public virtual PgJsonTraversalExpression Update(SqlExpression expression, IReadOnlyList path) - => expression == Expression && path.Count == Path.Count && path.Zip(Path, (x, y) => (x, y)).All(tup => tup.x == tup.y) - ? 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. - /// - public virtual PgJsonTraversalExpression Append(SqlExpression pathComponent) - { - var newPath = new SqlExpression[Path.Count + 1]; - for (var i = 0; i < Path.Count(); i++) - { - newPath[i] = Path[i]; - } - - newPath[newPath.Length - 1] = pathComponent; - return new PgJsonTraversalExpression(Expression, newPath, ReturnsText, Type, TypeMapping); - } - - /// - public override bool Equals(object? obj) - => Equals(obj as PgJsonTraversalExpression); - - /// - public virtual bool Equals(PgJsonTraversalExpression? other) - => ReferenceEquals(this, other) - || other is not null - && base.Equals(other) - && Equals(Expression, other.Expression) - && Path.Count == other.Path.Count - && Path.Zip(other.Path, (x, y) => (x, y)).All(tup => tup.x == tup.y); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Expression, Path); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Expression); - expressionPrinter.Append(ReturnsText ? "#>>" : "#>"); - expressionPrinter.Append("{"); - for (var i = 0; i < Path.Count; i++) - { - expressionPrinter.Visit(Path[i]); - if (i < Path.Count - 1) - { - expressionPrinter.Append(","); - } - } - - expressionPrinter.Append("}"); - } - - /// - public override string ToString() - => $"{Expression}{(ReturnsText ? "#>>" : "#>")}{Path}"; -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs deleted file mode 100644 index 22cfe208de..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgNewArrayExpression.cs +++ /dev/null @@ -1,135 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// Represents creating a new PostgreSQL array. -/// -public class PgNewArrayExpression : SqlExpression -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// Creates a new instance of the class. - /// - /// The values to initialize the elements of the new array. - /// The of the expression. - /// The associated with the expression. - public PgNewArrayExpression( - IReadOnlyList expressions, - Type type, - RelationalTypeMapping? typeMapping) - : base(type, typeMapping) - { - Check.NotNull(expressions, nameof(expressions)); - - if (type.TryGetElementType(typeof(IEnumerable<>)) is null) - { - throw new ArgumentException($"{nameof(PgNewArrayExpression)} must have an IEnumerable type"); - } - - Expressions = expressions; - } - - /// - /// The operator of this PostgreSQL binary operation. - /// - public virtual IReadOnlyList Expressions { get; } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - List? newExpressions = null; - for (var i = 0; i < Expressions.Count; i++) - { - var expression = Expressions[i]; - var visitedExpression = (SqlExpression)visitor.Visit(expression); - if (visitedExpression != expression && newExpressions is null) - { - newExpressions = []; - for (var j = 0; j < i; j++) - { - newExpressions.Add(Expressions[j]); - } - } - - newExpressions?.Add(visitedExpression); - } - - return newExpressions is null - ? this - : new PgNewArrayExpression(newExpressions, Type, TypeMapping); - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The values to initialize the elements of the new array. - /// This expression if no children changed, or an expression with the updated children. - public virtual PgNewArrayExpression Update(IReadOnlyList expressions) - { - Check.NotNull(expressions, nameof(expressions)); - - return expressions == Expressions - ? this - : 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) - { - Check.NotNull(expressionPrinter, nameof(expressionPrinter)); - - expressionPrinter.Append("ARRAY["); - - var first = true; - foreach (var expression in Expressions) - { - if (!first) - { - expressionPrinter.Append(", "); - } - - first = false; - - expressionPrinter.Visit(expression); - } - - expressionPrinter.Append("]"); - } - - /// - public override bool Equals(object? obj) - => obj is not null - && (ReferenceEquals(this, obj) - || obj is PgNewArrayExpression sqlBinaryExpression - && Equals(sqlBinaryExpression)); - - private bool Equals(PgNewArrayExpression pgNewArrayExpression) - => base.Equals(pgNewArrayExpression) - && Expressions.SequenceEqual(pgNewArrayExpression.Expressions); - - /// - public override int GetHashCode() - { - var hash = new HashCode(); - - hash.Add(base.GetHashCode()); - for (var i = 0; i < Expressions.Count; i++) - { - hash.Add(Expressions[i]); - } - - return hash.ToHashCode(); - } -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs deleted file mode 100644 index f907e7b42d..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgRegexMatchExpression.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// Represents a PostgreSQL regular expression match expression. -/// -public class PgRegexMatchExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - public override Type Type - => typeof(bool); - - /// - /// The match expression. - /// - public virtual SqlExpression Match { get; } - - /// - /// The pattern to match. - /// - public virtual SqlExpression Pattern { get; } - - /// - /// The options for regular expression evaluation. - /// - public virtual RegexOptions Options { get; } - - /// - /// Constructs a . - /// - /// The expression to match. - /// The pattern to match. - /// The options for regular expression evaluation. - /// The type mapping for the expression. - public PgRegexMatchExpression( - SqlExpression match, - SqlExpression pattern, - RegexOptions options, - RelationalTypeMapping? typeMapping) - : base(typeof(bool), typeMapping) - { - Match = match; - Pattern = pattern; - Options = options; - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update((SqlExpression)visitor.Visit(Match), (SqlExpression)visitor.Visit(Pattern)); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - public virtual PgRegexMatchExpression Update(SqlExpression match, SqlExpression pattern) - => match != Match || pattern != Pattern - ? 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) - || other is not null - && base.Equals(other) - && Match.Equals(other.Match) - && Pattern.Equals(other.Pattern) - && Options.Equals(other.Options); - - /// - public override bool Equals(object? other) - => other is PgRegexMatchExpression otherRegexMatch && Equals(otherRegexMatch); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Match, Pattern, Options); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Match); - expressionPrinter.Append(" ~ "); - expressionPrinter.Visit(Pattern); - } - - /// - public override string ToString() - => $"{Match} ~ {Pattern}"; -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs deleted file mode 100644 index 0d0e6f3058..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgRowValueExpression.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// An expression that represents a PostgreSQL-specific row value expression in a SQL tree. -/// -/// -/// See the PostgreSQL docs -/// for more information. -/// -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) - : base(type, typeMapping) - { - Check.NotNull(values, nameof(values)); - Check.DebugAssert(type.IsAssignableTo(typeof(ITuple)), $"Type '{type}' isn't an ITuple"); - - Values = values; - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - SqlExpression[]? newRowValues = null; - - for (var i = 0; i < Values.Count; i++) - { - var rowValue = Values[i]; - var visited = (SqlExpression)visitor.Visit(rowValue); - if (visited != rowValue && newRowValues is null) - { - newRowValues = new SqlExpression[Values.Count]; - for (var j = 0; j < i; j++) - { - newRowValues[j] = Values[j]; - } - } - - if (newRowValues is not null) - { - newRowValues[i] = visited; - } - } - - return newRowValues is null ? this : new PgRowValueExpression(newRowValues, Type); - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - public virtual PgRowValueExpression Update(IReadOnlyList values) - => values.Count == Values.Count && values.Zip(Values, (x, y) => (x, y)).All(tup => tup.x == tup.y) - ? 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) - { - expressionPrinter.Append("("); - - var count = Values.Count; - for (var i = 0; i < count; i++) - { - expressionPrinter.Visit(Values[i]); - - if (i < count - 1) - { - expressionPrinter.Append(", "); - } - } - - expressionPrinter.Append(")"); - } - - /// - public override bool Equals(object? obj) - => obj is PgRowValueExpression other && Equals(other); - - /// - public virtual bool Equals(PgRowValueExpression? other) - { - if (other is null || !base.Equals(other) || other.Values.Count != Values.Count) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - for (var i = 0; i < Values.Count; i++) - { - if (!other.Values[i].Equals(Values[i])) - { - return false; - } - } - - return true; - } - - /// - public override int GetHashCode() - { - var hashCode = new HashCode(); - - foreach (var rowValue in Values) - { - hashCode.Add(rowValue); - } - - return hashCode.ToHashCode(); - } -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs deleted file mode 100644 index 86bcd95d9e..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs +++ /dev/null @@ -1,174 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// An expression that represents a PostgreSQL unnest function call in a SQL tree. -/// -/// -/// -/// This expression is just a , adding the ability to provide an explicit column name -/// for its output (SELECT * FROM unnest(array) AS f(foo)). This is necessary since when the column name isn't explicitly -/// specified, it is automatically identical to the table alias (f above); since the table alias may get uniquified by -/// EF, this would break queries. -/// -/// -/// See unnest for more -/// information and examples. -/// -/// -/// 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 PgTableValuedFunctionExpression : TableValuedFunctionExpression, IEquatable -{ - /// - /// The name of the column to be projected out from the unnest call. - /// - /// - /// 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 IReadOnlyList? ColumnInfos { get; } - - /// - /// Whether to project an additional ordinality column containing the index of each element in the array. - /// - /// - /// 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 bool WithOrdinality { 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 PgTableValuedFunctionExpression( - string alias, - string name, - IReadOnlyList arguments, - IReadOnlyList? columnInfos = null, - bool withOrdinality = true) - : base(alias, name, schema: null, builtIn: true, arguments) - { - ColumnInfos = columnInfos; - WithOrdinality = withOrdinality; - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => visitor.VisitAndConvert(Arguments) is var visitedArguments && visitedArguments == Arguments - ? this - : new PgTableValuedFunctionExpression(Alias, Name, visitedArguments, 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 override PgTableValuedFunctionExpression Update(IReadOnlyList arguments) - => 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) - { - expressionPrinter.Append(Name); - expressionPrinter.Append("("); - expressionPrinter.VisitCollection(Arguments); - expressionPrinter.Append(")"); - - if (WithOrdinality) - { - expressionPrinter.Append(" WITH ORDINALITY"); - } - - PrintAnnotations(expressionPrinter); - - expressionPrinter.Append(" AS ").Append(Alias); - - if (ColumnInfos is not null) - { - expressionPrinter.Append("("); - - var isFirst = true; - - foreach (var column in ColumnInfos) - { - if (isFirst) - { - isFirst = false; - } - else - { - expressionPrinter.Append(", "); - } - - expressionPrinter.Append(column.Name); - - if (column.TypeMapping is not null) - { - expressionPrinter.Append(" ").Append(column.TypeMapping.StoreType); - } - } - - expressionPrinter.Append(")"); - } - } - - /// - public override bool Equals(object? obj) - => ReferenceEquals(obj, this) || obj is PgTableValuedFunctionExpression e && Equals(e); - - /// - public bool Equals(PgTableValuedFunctionExpression? expression) - => base.Equals(expression) - && ( - expression.ColumnInfos is null && ColumnInfos is null - || expression.ColumnInfos is not null && ColumnInfos is not null && expression.ColumnInfos.SequenceEqual(ColumnInfos)) - && WithOrdinality == expression.WithOrdinality; - - /// - public override int GetHashCode() - => base.GetHashCode(); - - /// - /// Defines the name of a column coming out of a and optionally its type. - /// - public readonly record struct ColumnInfo(string Name, RelationalTypeMapping? TypeMapping = null); -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs deleted file mode 100644 index 2d8dd0ee77..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgUnknownBinaryExpression.cs +++ /dev/null @@ -1,99 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// A binary expression only to be used by plugins, since new expressions can only be added (and handled) -/// within the provider itself. Allows defining the operator as a string within the expression, and has -/// default (i.e. propagating) nullability semantics. -/// All type mappings must be applied to the operands before the expression is constructed, since there's -/// no inference logic for it in . -/// -public class PgUnknownBinaryExpression : SqlExpression, IEquatable -{ - private static ConstructorInfo? _quotingConstructor; - - /// - /// The left-hand expression. - /// - public virtual SqlExpression Left { get; } - - /// - /// The right-hand expression. - /// - public virtual SqlExpression Right { get; } - - /// - /// The operator. - /// - public virtual string Operator { get; } - - /// - /// Constructs a . - /// - /// The left-hand expression. - /// The right-hand expression. - /// The operator symbol acting on the expression. - /// The result type. - /// The type mapping for the expression. - /// - public PgUnknownBinaryExpression( - SqlExpression left, - SqlExpression right, - string binaryOperator, - Type type, - RelationalTypeMapping? typeMapping = null) - : base(type, typeMapping) - { - Left = Check.NotNull(left, nameof(left)); - Right = Check.NotNull(right, nameof(right)); - Operator = Check.NotEmpty(binaryOperator, nameof(binaryOperator)); - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update((SqlExpression)visitor.Visit(Left), (SqlExpression)visitor.Visit(Right)); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - public virtual PgUnknownBinaryExpression Update(SqlExpression left, SqlExpression right) - => left == Left && right == Right - ? 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) - || other is not null && Left.Equals(other.Left) && Right.Equals(other.Right) && Operator == other.Operator; - - /// - public override bool Equals(object? obj) - => obj is PgUnknownBinaryExpression e && Equals(e); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Left, Right, Operator); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Visit(Left); - expressionPrinter.Append(Operator); - expressionPrinter.Visit(Right); - } - - /// - public override string ToString() - => $"{Left} {Operator} {Right}"; -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs deleted file mode 100644 index 5d1a6b31a7..0000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs +++ /dev/null @@ -1,114 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -/// -/// An expression that represents a PostgreSQL unnest function call in a SQL tree. -/// -/// -/// -/// This expression is just a , adding the ability to provide an explicit column name -/// for its output (SELECT * FROM unnest(array) AS f(foo)). This is necessary since when the column name isn't explicitly -/// specified, it is automatically identical to the table alias (f above); since the table alias may get uniquified by -/// EF, this would break queries. -/// -/// -/// See unnest for more -/// information and examples. -/// -/// -/// 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 PgUnnestExpression : PgTableValuedFunctionExpression -{ - /// - /// The array to be un-nested into a table. - /// - /// - /// 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 Array - => Arguments[0]; - - /// - /// The name of the column to be projected out from the unnest call. - /// - /// - /// 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 string ColumnName - => ColumnInfos![0].Name; - - /// - /// 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 PgUnnestExpression(string alias, SqlExpression array, string columnName, bool withOrdinality = true) - : 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) - { - } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => visitor.Visit(Array) is var visitedArray && visitedArray == Array - ? this - : new PgUnnestExpression(Alias, (SqlExpression)visitedArray, ColumnName, 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 override PgUnnestExpression Update(IReadOnlyList arguments) - => arguments is [var singleArgument] - ? Update(singleArgument) - : throw new ArgumentException(); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - 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/Expressions/PgExpressionType.cs b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs deleted file mode 100644 index 57e1176823..0000000000 --- a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs +++ /dev/null @@ -1,186 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; - -/// -/// PostgreSQL-specific expression node types. -/// -public enum PgExpressionType -{ - #region General operators - - /// - /// Represents a PostgreSQL contains operator. - /// - Contains, // >> (inet/cidr), @> - - /// - /// Represents a PostgreSQL contained-by operator. - /// - ContainedBy, // << (inet/cidr), <@ - - /// - /// Represents a PostgreSQL overlap operator. - /// - Overlaps, // && - - /// - /// Represents a PostgreSQL operator for finding the distance between two things (e.g. 2D distance between two geometries, - /// between timestamps...) - /// - Distance, // <-> - - #endregion General operators - - #region Network - - /// - /// Represents a PostgreSQL network contained-by-or-equal operator. - /// - NetworkContainedByOrEqual, // <<= - - /// - /// Represents a PostgreSQL network contains-or-equal operator. - /// - NetworkContainsOrEqual, // >>= - - /// - /// Represents a PostgreSQL network contains-or-contained-by operator. - /// - NetworkContainsOrContainedBy, // && - - #endregion Network - - #region Range - - /// - /// Represents a PostgreSQL operator for checking if a range is strictly to the left of another range. - /// - RangeIsStrictlyLeftOf, // << - - /// - /// Represents a PostgreSQL operator for checking if a range is strictly to the right of another range. - /// - RangeIsStrictlyRightOf, // >> - - /// - /// Represents a PostgreSQL operator for checking if a range does not extend to the right of another range. - /// - RangeDoesNotExtendRightOf, // &< - - /// - /// Represents a PostgreSQL operator for checking if a range does not extend to the left of another range. - /// - RangeDoesNotExtendLeftOf, // &> - - /// - /// Represents a PostgreSQL operator for checking if a range is adjacent to another range. - /// - RangeIsAdjacentTo, // -|- - - /// - /// Represents a PostgreSQL operator for performing a union between two ranges. - /// - RangeUnion, // + - - /// - /// Represents a PostgreSQL operator for performing an intersection between two ranges. - /// - RangeIntersect, // * - - /// - /// Represents a PostgreSQL operator for performing an except operation between two ranges. - /// - RangeExcept, // - - - #endregion Range - - #region Text search - - /// - /// Represents a PostgreSQL operator for performing a full-text search match. - /// - TextSearchMatch, // @@ - - /// - /// Represents a PostgreSQL operator for logical AND within a full-text search match. - /// - TextSearchAnd, // && - - /// - /// Represents a PostgreSQL operator for logical OR within a full-text search match. - /// - TextSearchOr, // || - - #endregion Text search - - #region JSON - - /// - /// Represents a PostgreSQL operator for checking whether a key exists in a JSON document. - /// - JsonExists, // ? - - /// - /// Represents a PostgreSQL operator for checking whether any of multiple keys exists in a JSON document. - /// - JsonExistsAny, // ?@> - - /// - /// Represents a PostgreSQL operator for checking whether all the given keys exist in a JSON document. - /// - JsonExistsAll, // ?<@ - - #endregion JSON - - #region LTree - - /// - /// Represents a PostgreSQL operator for matching in an ltree type. - /// - LTreeMatches, // ~ or @ - - /// - /// Represents a PostgreSQL operator for matching in an ltree type. - /// - LTreeMatchesAny, // ? - - /// - /// Represents a PostgreSQL operator for finding the first ancestor in an ltree type. - /// - LTreeFirstAncestor, // ?@> - - /// - /// Represents a PostgreSQL operator for finding the first descendent in an ltree type. - /// - LTreeFirstDescendent, // ?<@ - - /// - /// Represents a PostgreSQL operator for finding the first match in an ltree type. - /// - LTreeFirstMatches, // ?~ or ?@ - - #endregion LTree - - #region Cube - - /// - /// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube. - /// - CubeNthCoordinate, // -> - - /// - /// Represents a PostgreSQL operator for extracting the n-th coordinate of a cube for KNN indexing. - /// - CubeNthCoordinateKnn, // ~> - - /// - /// Represents a PostgreSQL operator for computing the taxicab (L-1 metric) distance between two cubes. - /// - CubeDistanceTaxicab, // <#> - - /// - /// Represents a PostgreSQL operator for computing the Chebyshev (L-inf metric) distance between two cubes. - /// - CubeDistanceChebyshev, // <=> - - #endregion Cube -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs deleted file mode 100644 index 2b57316982..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlCompiledQueryCacheKeyGenerator.cs +++ /dev/null @@ -1,56 +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 NpgsqlCompiledQueryCacheKeyGenerator : RelationalCompiledQueryCacheKeyGenerator -{ - /// - /// 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 NpgsqlCompiledQueryCacheKeyGenerator( - CompiledQueryCacheKeyGeneratorDependencies dependencies, - RelationalCompiledQueryCacheKeyGeneratorDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } - - /// - /// 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 object GenerateCacheKey(Expression query, bool async) - => new NpgsqlCompiledQueryCacheKey( - GenerateCacheKeyCore(query, async), - RelationalDependencies.ContextOptions.FindExtension()?.ReverseNullOrdering ?? false); - - private struct NpgsqlCompiledQueryCacheKey( - RelationalCompiledQueryCacheKey relationalCompiledQueryCacheKey, - bool reverseNullOrdering) - { - private readonly RelationalCompiledQueryCacheKey _relationalCompiledQueryCacheKey = relationalCompiledQueryCacheKey; - private readonly bool _reverseNullOrdering = reverseNullOrdering; - - public override bool Equals(object? obj) - => !(obj is null) - && obj is NpgsqlCompiledQueryCacheKey key - && Equals(key); - - private bool Equals(NpgsqlCompiledQueryCacheKey other) - => _relationalCompiledQueryCacheKey.Equals(other._relationalCompiledQueryCacheKey) - && _reverseNullOrdering == other._reverseNullOrdering; - - public override int GetHashCode() - => HashCode.Combine(_relationalCompiledQueryCacheKey, _reverseNullOrdering); - } -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs deleted file mode 100644 index 4c3505e150..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlDeleteConvertingExpressionVisitor.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; - -/// -/// Converts the relational into a PG-specific , which -/// precisely models a DELETE statement in PostgreSQL. This is done to handle the PG-specific USING syntax for table joining. -/// -public class NpgsqlDeleteConvertingExpressionVisitor : 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. - /// - public virtual Expression Process(Expression node) - => node switch - { - DeleteExpression deleteExpression => VisitDelete(deleteExpression), - - _ => node - }; - - /// - /// 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 Expression VisitDelete(DeleteExpression deleteExpression) - { - var selectExpression = deleteExpression.SelectExpression; - - if (selectExpression.Offset != null - || selectExpression.Limit != null - || selectExpression.Having != null - || selectExpression.Orderings.Count > 0 - || selectExpression.GroupBy.Count > 0 - || selectExpression.Projection.Count > 0) - { - throw new InvalidOperationException( - RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( - nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); - } - - var fromItems = new List(); - SqlExpression? joinPredicates = null; - - // The SelectExpression also contains the target table being modified (same as deleteExpression.Table). - // If it has additional inner joins, use the PostgreSQL-specific USING syntax to express the join. - // Note that the non-join TableExpression isn't necessary the target table - through projection the last table being - // joined may be the one being modified. - foreach (var tableBase in selectExpression.Tables) - { - switch (tableBase) - { - case TableExpression tableExpression: - if (tableExpression.Alias != deleteExpression.Table.Alias) - { - fromItems.Add(tableExpression); - } - - break; - - case InnerJoinExpression { Table: { } tableExpression } innerJoinExpression: - if (tableExpression.Alias != deleteExpression.Table.Alias) - { - fromItems.Add(tableExpression); - } - - joinPredicates = joinPredicates is null - ? innerJoinExpression.JoinPredicate - : new SqlBinaryExpression( - ExpressionType.AndAlso, joinPredicates, innerJoinExpression.JoinPredicate, typeof(bool), - innerJoinExpression.JoinPredicate.TypeMapping); - break; - - default: - throw new InvalidOperationException( - RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( - nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); - } - } - - // Combine the join predicates (if any) before the user-provided predicate - var predicate = (joinPredicates, selectExpression.Predicate) switch - { - (null, not null) => selectExpression.Predicate, - (not null, null) => joinPredicates, - (null, null) => null, - (not null, not null) => new SqlBinaryExpression( - ExpressionType.AndAlso, joinPredicates, selectExpression.Predicate, typeof(bool), joinPredicates.TypeMapping) - }; - - return new PgDeleteExpression(deleteExpression.Table, fromItems, predicate, deleteExpression.Tags); - } -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs deleted file mode 100644 index ffb9528260..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Runtime.CompilerServices; -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 NpgsqlEvaluatableExpressionFilter : RelationalEvaluatableExpressionFilter -{ - private readonly Version _postgresVersion; - - private static readonly MethodInfo TsQueryParse = - typeof(NpgsqlTsQuery).GetRuntimeMethod(nameof(NpgsqlTsQuery.Parse), [typeof(string)])!; - - private static readonly MethodInfo TsVectorParse = - typeof(NpgsqlTsVector).GetRuntimeMethod(nameof(NpgsqlTsVector.Parse), [typeof(string)])!; - - /// - /// 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 NpgsqlEvaluatableExpressionFilter( - EvaluatableExpressionFilterDependencies dependencies, - RelationalEvaluatableExpressionFilterDependencies relationalDependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies, relationalDependencies) - { - _postgresVersion = npgsqlSingletonOptions.PostgresVersion; - } - - /// - /// 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 bool IsEvaluatableExpression(Expression expression, IModel model) - { - switch (expression) - { - case MethodCallExpression methodCallExpression: - var declaringType = methodCallExpression.Method.DeclaringType; - var method = methodCallExpression.Method; - - if (method == TsQueryParse - || method == TsVectorParse - || declaringType == typeof(NpgsqlDbFunctionsExtensions) - || declaringType == typeof(NpgsqlFullTextSearchDbFunctionsExtensions) - || declaringType == typeof(NpgsqlFullTextSearchLinqExtensions) - || declaringType == typeof(NpgsqlNetworkDbFunctionsExtensions) - || declaringType == typeof(NpgsqlJsonDbFunctionsExtensions) - || declaringType == typeof(NpgsqlRangeDbFunctionsExtensions) - // PG18 introduced uuidv7(), so we prevent local evaluation when targeting PG18 or later. - || declaringType == typeof(Guid) && method.Name == nameof(Guid.CreateVersion7) && _postgresVersion.AtLeast(18) - // Prevent evaluation of ValueTuple.Create, see NewExpression of ITuple below - || declaringType == typeof(ValueTuple) && method.Name == nameof(ValueTuple.Create)) - { - return false; - } - - break; - - case NewExpression newExpression when newExpression.Type.IsAssignableTo(typeof(ITuple)): - // We translate new ValueTuple(x, y...) to a SQL row value expression: (x, y) - // (see NpgsqlSqlTranslatingExpressionVisitor.VisitNew). - // We must prevent evaluation when the tuple contains only constants/parameters, since SQL row values cannot be - // parameterized; we need to render them as "literals" instead: - // WHERE (x, y) > (3, $1) - return false; - } - - return base.IsEvaluatableExpression(expression, model); - } -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs deleted file mode 100644 index 8405e12fde..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs +++ /dev/null @@ -1,42 +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 NpgsqlParameterBasedSqlProcessor : RelationalParameterBasedSqlProcessor -{ - /// - /// 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 NpgsqlParameterBasedSqlProcessor( - RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters) - : base(dependencies, parameters) - { - } - - /// - /// 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 Expression Process(Expression queryExpression, ParametersCacheDecorator parametersDecorator) - { - queryExpression = base.Process(queryExpression, parametersDecorator); - - queryExpression = new NpgsqlDeleteConvertingExpressionVisitor().Process(queryExpression); - - return queryExpression; - } - - /// - protected override Expression ProcessSqlNullability(Expression selectExpression, ParametersCacheDecorator parametersDecorator) - => new NpgsqlSqlNullabilityProcessor(Dependencies, Parameters).Process(selectExpression, parametersDecorator); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs deleted file mode 100644 index f4cf99980f..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessorFactory.cs +++ /dev/null @@ -1,32 +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 NpgsqlParameterBasedSqlProcessorFactory : IRelationalParameterBasedSqlProcessorFactory -{ - private readonly RelationalParameterBasedSqlProcessorDependencies _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 - /// 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 NpgsqlParameterBasedSqlProcessorFactory( - RelationalParameterBasedSqlProcessorDependencies dependencies) - { - _dependencies = dependencies; - } - - /// - /// Creates a new . - /// - /// 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 deleted file mode 100644 index a04bb43c54..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContext.cs +++ /dev/null @@ -1,51 +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 NpgsqlQueryCompilationContext : RelationalQueryCompilationContext -{ - /// - /// 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) - : this(dependencies, relationalDependencies, async, precompiling: false) - { - } - - /// - /// 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) - : base(dependencies, relationalDependencies, async, precompiling) - { - } - - /// - /// 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 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 deleted file mode 100644 index cc35642a33..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryCompilationContextFactory.cs +++ /dev/null @@ -1,48 +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 NpgsqlQueryCompilationContextFactory : IQueryCompilationContextFactory -{ - private readonly QueryCompilationContextDependencies _dependencies; - private readonly RelationalQueryCompilationContextDependencies _relationalDependencies; - - /// - /// 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 NpgsqlQueryCompilationContextFactory( - QueryCompilationContextDependencies dependencies, - RelationalQueryCompilationContextDependencies relationalDependencies) - { - Check.NotNull(dependencies, nameof(dependencies)); - Check.NotNull(relationalDependencies, nameof(relationalDependencies)); - - _dependencies = dependencies; - _relationalDependencies = relationalDependencies; - } - - /// - /// 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 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) - => new NpgsqlQueryCompilationContext(_dependencies, _relationalDependencies, async, precompiling: true); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs deleted file mode 100644 index 6dc0c2c2cb..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ /dev/null @@ -1,1623 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.NetworkInformation; -using System.Text.RegularExpressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; - -/// -/// The default query SQL generator for Npgsql. -/// -public class NpgsqlQuerySqlGenerator : QuerySqlGenerator -{ - private readonly ISqlGenerationHelper _sqlGenerationHelper; - private readonly IRelationalTypeMappingSource _typeMappingSource; - private RelationalTypeMapping? _textTypeMapping; - - /// - /// True if null ordering is reversed; otherwise false. - /// - private readonly bool _reverseNullOrderingEnabled; - - /// - /// The backend version to target. If null, it means the user hasn't set a compatibility version, and the - /// latest should be targeted. - /// - private readonly Version _postgresVersion; - - /// - public NpgsqlQuerySqlGenerator( - QuerySqlGeneratorDependencies dependencies, - IRelationalTypeMappingSource typeMappingSource, - bool reverseNullOrderingEnabled, - Version postgresVersion) - : base(dependencies) - { - _sqlGenerationHelper = dependencies.SqlGenerationHelper; - _typeMappingSource = typeMappingSource; - _reverseNullOrderingEnabled = reverseNullOrderingEnabled; - _postgresVersion = postgresVersion; - } - - /// - /// 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 - { - PgAllExpression e => VisitArrayAll(e), - PgAnyExpression e => VisitArrayAny(e), - PgArrayIndexExpression e => VisitArrayIndex(e), - PgArraySliceExpression e => VisitArraySlice(e), - PgBinaryExpression e => VisitPgBinary(e), - PgDeleteExpression e => VisitPgDelete(e), - PgFunctionExpression e => VisitPgFunction(e), - PgILikeExpression e => VisitILike(e), - PgJsonTraversalExpression e => VisitJsonPathTraversal(e), - PgNewArrayExpression e => VisitNewArray(e), - PgRegexMatchExpression e => VisitRegexMatch(e), - PgRowValueExpression e => VisitRowValue(e), - PgUnknownBinaryExpression e => VisitUnknownBinary(e), - PgTableValuedFunctionExpression e => VisitPgTableValuedFunctionExpression(e), - - _ => base.VisitExtension(extensionExpression) - }; - - /// - protected override void GenerateRootCommand(Expression queryExpression) - { - switch (queryExpression) - { - case PgDeleteExpression postgresDeleteExpression: - GenerateTagsHeaderComment(postgresDeleteExpression.Tags); - VisitPgDelete(postgresDeleteExpression); - break; - - default: - base.GenerateRootCommand(queryExpression); - break; - } - } - - /// - protected override void GenerateLimitOffset(SelectExpression selectExpression) - { - Check.NotNull(selectExpression, nameof(selectExpression)); - - if (selectExpression.Limit is not null) - { - Sql.AppendLine().Append("LIMIT "); - Visit(selectExpression.Limit); - } - - if (selectExpression.Offset is not null) - { - if (selectExpression.Limit is null) - { - Sql.AppendLine(); - } - else - { - Sql.Append(" "); - } - - Sql.Append("OFFSET "); - Visit(selectExpression.Offset); - } - } - - /// - protected override string GetOperator(SqlBinaryExpression e) - => e.OperatorType switch - { - // PostgreSQL has a special string concatenation operator: || - // We switch to it if the expression itself has type string, or if one of the sides has a string type mapping. - // Same for full-text search's TsVector, arrays. - ExpressionType.Add when - e.Type == typeof(string) - || e.Left.TypeMapping?.ClrType == typeof(string) - || e.Right.TypeMapping?.ClrType == typeof(string) - || e.Type == typeof(NpgsqlTsVector) - || e.Left.TypeMapping?.ClrType == typeof(NpgsqlTsVector) - || e.Right.TypeMapping?.ClrType == typeof(NpgsqlTsVector) - || e.Left.TypeMapping is NpgsqlArrayTypeMapping && e.Right.TypeMapping is NpgsqlArrayTypeMapping - => " || ", - - 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) - }; - - /// - protected override Expression VisitOrdering(OrderingExpression ordering) - { - var result = base.VisitOrdering(ordering); - - if (_reverseNullOrderingEnabled) - { - Sql.Append(ordering.IsAscending ? " NULLS FIRST" : " NULLS LAST"); - } - - return result; - } - - /// - protected override void GenerateTop(SelectExpression selectExpression) - { - // No TOP() in PostgreSQL, see GenerateLimitOffset - } - - /// - /// Generates SQL for a constant. - /// - /// The for which to generate SQL. - protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) - { - // Certain JSON functions (e.g. jsonb_set()) accept a JSONPATH argument - this is (currently) flown here as a - // SqlConstantExpression over IReadOnlyList. Render that to a string here. - if (sqlConstantExpression is { Value: IReadOnlyList path }) - { - GenerateJsonPath(ConvertJsonPathSegments(path)); - return sqlConstantExpression; - } - - return base.VisitSqlConstant(sqlConstantExpression); - } - - /// - /// 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 VisitCrossApply(CrossApplyExpression crossApplyExpression) - { - Sql.Append("JOIN LATERAL "); - - if (crossApplyExpression.Table is TableExpression table) - { - // PostgreSQL doesn't support LATERAL JOIN over table, and it doesn't really make sense to do it - but EF Core - // will sometimes generate that. #1560 - Sql - .Append("(SELECT * FROM ") - .Append(_sqlGenerationHelper.DelimitIdentifier(table.Name, table.Schema)) - .Append(")") - .Append(AliasSeparator) - .Append(_sqlGenerationHelper.DelimitIdentifier(table.Alias)); - } - else - { - Visit(crossApplyExpression.Table); - } - - Sql.Append(" ON TRUE"); - return crossApplyExpression; - } - - /// - /// 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 VisitOuterApply(OuterApplyExpression outerApplyExpression) - { - Sql.Append("LEFT JOIN LATERAL "); - - if (outerApplyExpression.Table is TableExpression table) - { - // PostgreSQL doesn't support LATERAL JOIN over table, and it doesn't really make sense to do it - but EF Core - // will sometimes generate that. #1560 - Sql - .Append("(SELECT * FROM ") - .Append(_sqlGenerationHelper.DelimitIdentifier(table.Name, table.Schema)) - .Append(")") - .Append(AliasSeparator) - .Append(_sqlGenerationHelper.DelimitIdentifier(table.Alias)); - } - else - { - Visit(outerApplyExpression.Table); - } - - Sql.Append(" ON TRUE"); - return outerApplyExpression; - } - - /// - protected override Expression VisitSqlBinary(SqlBinaryExpression binary) - { - switch (binary.OperatorType) - { - case ExpressionType.Add: - { - if (_postgresVersion >= new Version(9, 5)) - { - return base.VisitSqlBinary(binary); - } - - // PostgreSQL 9.4 and below has some weird operator precedence fixed in 9.5 and described here: - // http://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=c6b3c939b7e0f1d35f4ed4996e71420a993810d2 - // As a result we must surround string concatenation with parentheses - if (binary.Left.Type == typeof(string) && binary.Right.Type == typeof(string)) - { - Sql.Append("("); - var exp = base.VisitSqlBinary(binary); - Sql.Append(")"); - return exp; - } - - return base.VisitSqlBinary(binary); - } - - case ExpressionType.ArrayIndex: - return VisitArrayIndex(binary); - - default: - return base.VisitSqlBinary(binary); - } - } - - // NonQueryConvertingExpressionVisitor converts the relational DeleteExpression to PostgresDeleteExpression, so we should never - // get here - /// - /// 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 VisitDelete(DeleteExpression deleteExpression) - => throw new InvalidOperationException("Inconceivable!"); - - /// - /// 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 Expression VisitPgDelete(PgDeleteExpression pgDeleteExpression) - { - Sql.Append("DELETE FROM "); - Visit(pgDeleteExpression.Table); - - if (pgDeleteExpression.FromItems.Count > 0) - { - Sql.AppendLine().Append("USING "); - GenerateList(pgDeleteExpression.FromItems, t => Visit(t), sql => sql.Append(", ")); - } - - if (pgDeleteExpression.Predicate != null) - { - Sql.AppendLine().Append("WHERE "); - Visit(pgDeleteExpression.Predicate); - } - - return pgDeleteExpression; - } - - /// - /// 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 VisitUpdate(UpdateExpression updateExpression) - { - var selectExpression = updateExpression.SelectExpression; - - if (selectExpression.Offset == null - && selectExpression.Limit == null - && selectExpression.Having == null - && selectExpression.Orderings.Count == 0 - && selectExpression.GroupBy.Count == 0 - && selectExpression.Projection.Count == 0 - && (selectExpression.Tables.Count == 1 - || !ReferenceEquals(selectExpression.Tables[0], updateExpression.Table) - || selectExpression.Tables[1] is InnerJoinExpression - || selectExpression.Tables[1] is CrossJoinExpression)) - { - Sql.Append("UPDATE "); - Visit(updateExpression.Table); - Sql.AppendLine(); - Sql.Append("SET "); - Sql.Append( - $"{_sqlGenerationHelper.DelimitIdentifier(updateExpression.ColumnValueSetters[0].Column.Name)} = "); - Visit(updateExpression.ColumnValueSetters[0].Value); - using (Sql.Indent()) - { - foreach (var columnValueSetter in updateExpression.ColumnValueSetters.Skip(1)) - { - Sql.AppendLine(","); - Sql.Append($"{_sqlGenerationHelper.DelimitIdentifier(columnValueSetter.Column.Name)} = "); - Visit(columnValueSetter.Value); - } - } - - var predicate = selectExpression.Predicate; - var firstTable = true; - OuterReferenceFindingExpressionVisitor? visitor = null; - - if (selectExpression.Tables.Count > 1) - { - Sql.AppendLine().Append("FROM "); - - for (var i = 0; i < selectExpression.Tables.Count; i++) - { - var table = selectExpression.Tables[i]; - var joinExpression = table as JoinExpressionBase; - - if (updateExpression.Table.Alias == (joinExpression?.Table.Alias ?? table.Alias)) - { - LiftPredicate(table); - continue; - } - - visitor ??= new OuterReferenceFindingExpressionVisitor(updateExpression.Table); - - // PostgreSQL doesn't support referencing the main update table from anywhere except for the UPDATE WHERE clause. - // This specifically makes it impossible to have joins which reference the main table in their predicate (ON ...). - // Because of this, we detect all such inner joins and lift their predicates to the main WHERE clause (where a reference to the - // main table is allowed), producing UPDATE ... FROM x, y WHERE y.foreign_key = x.id instead of INNER JOIN ... ON. - if (firstTable) - { - LiftPredicate(table); - table = joinExpression?.Table ?? table; - } - else if (joinExpression is InnerJoinExpression innerJoinExpression - && visitor.ContainsReferenceToMainTable(innerJoinExpression.JoinPredicate)) - { - LiftPredicate(innerJoinExpression); - - Sql.AppendLine(","); - using (Sql.Indent()) - { - Visit(innerJoinExpression.Table); - } - - continue; - } - - if (firstTable) - { - firstTable = false; - } - else - { - Sql.AppendLine(); - } - - Visit(table); - - void LiftPredicate(TableExpressionBase joinTable) - { - if (joinTable is PredicateJoinExpressionBase predicateJoinExpression) - { - Check.DebugAssert(joinExpression is not LeftJoinExpression, "Cannot lift predicate for left join"); - - predicate = predicate == null - ? predicateJoinExpression.JoinPredicate - : new SqlBinaryExpression( - ExpressionType.AndAlso, - predicateJoinExpression.JoinPredicate, - predicate, - typeof(bool), - predicate.TypeMapping); - } - } - } - } - - if (predicate != null) - { - Sql.AppendLine().Append("WHERE "); - Visit(predicate); - } - - return updateExpression; - } - - throw new InvalidOperationException( - RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate))); - } - - /// - /// 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 Expression VisitNewArray(PgNewArrayExpression pgNewArrayExpression) - { - Debug.Assert(pgNewArrayExpression.TypeMapping is not null); - - Sql.Append("ARRAY["); - var first = true; - foreach (var initializer in pgNewArrayExpression.Expressions) - { - if (!first) - { - Sql.Append(","); - } - - first = false; - Visit(initializer); - } - - // Not sure if the explicit store type is necessary, but just to be sure. - Sql - .Append("]::") - .Append(pgNewArrayExpression.TypeMapping.StoreType); - - return pgNewArrayExpression; - } - - /// - /// 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 Expression VisitPgBinary(PgBinaryExpression binaryExpression) - { - Check.NotNull(binaryExpression, nameof(binaryExpression)); - - var requiresParentheses = RequiresParentheses(binaryExpression, binaryExpression.Left); - - if (requiresParentheses) - { - Sql.Append("("); - } - - Visit(binaryExpression.Left); - - if (requiresParentheses) - { - Sql.Append(")"); - } - - Debug.Assert(binaryExpression.Left.TypeMapping is not null); - Debug.Assert(binaryExpression.Right.TypeMapping is not null); - - Sql - .Append(" ") - .Append( - binaryExpression.OperatorType switch - { - PgExpressionType.Contains - when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTypeMapping or NpgsqlLegacyCidrTypeMapping - => ">>", - - PgExpressionType.ContainedBy - when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTypeMapping or NpgsqlLegacyCidrTypeMapping - => "<<", - - PgExpressionType.Contains => "@>", - PgExpressionType.ContainedBy => "<@", - PgExpressionType.Overlaps => "&&", - - PgExpressionType.NetworkContainedByOrEqual => "<<=", - PgExpressionType.NetworkContainsOrEqual => ">>=", - PgExpressionType.NetworkContainsOrContainedBy => "&&", - - PgExpressionType.RangeIsStrictlyLeftOf => "<<", - PgExpressionType.RangeIsStrictlyRightOf => ">>", - PgExpressionType.RangeDoesNotExtendRightOf => "&<", - PgExpressionType.RangeDoesNotExtendLeftOf => "&>", - PgExpressionType.RangeIsAdjacentTo => "-|-", - PgExpressionType.RangeUnion => "+", - PgExpressionType.RangeIntersect => "*", - PgExpressionType.RangeExcept => "-", - - PgExpressionType.TextSearchMatch => "@@", - PgExpressionType.TextSearchAnd => "&&", - PgExpressionType.TextSearchOr => "||", - - PgExpressionType.JsonExists => "?", - PgExpressionType.JsonExistsAny => "?|", - PgExpressionType.JsonExistsAll => "?&", - - PgExpressionType.LTreeMatches - when binaryExpression.Right.TypeMapping.StoreType == "lquery" - || binaryExpression.Right.TypeMapping is NpgsqlArrayTypeMapping { ElementTypeMapping.StoreType: "lquery" } => "~", - PgExpressionType.LTreeMatches - when binaryExpression.Right.TypeMapping.StoreType == "ltxtquery" - => "@", - PgExpressionType.LTreeMatchesAny => "?", - PgExpressionType.LTreeFirstAncestor => "?@>", - PgExpressionType.LTreeFirstDescendent => "?<@", - PgExpressionType.LTreeFirstMatches - when binaryExpression.Right.TypeMapping.StoreType == "lquery" => "?~", - PgExpressionType.LTreeFirstMatches - when binaryExpression.Right.TypeMapping.StoreType == "ltxtquery" => "?@", - - PgExpressionType.Distance => "<->", - - PgExpressionType.CubeNthCoordinate => "->", - PgExpressionType.CubeNthCoordinateKnn => "~>", - PgExpressionType.CubeDistanceTaxicab => "<#>", - PgExpressionType.CubeDistanceChebyshev => "<=>", - - _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}") - }) - .Append(" "); - - requiresParentheses = RequiresParentheses(binaryExpression, binaryExpression.Right); - - if (requiresParentheses) - { - Sql.Append("("); - } - - Visit(binaryExpression.Right); - - if (requiresParentheses) - { - Sql.Append(")"); - } - - return binaryExpression; - } - - /// - /// 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 Expression VisitArrayIndex(SqlBinaryExpression expression) - { - Visit(expression.Left); - Sql.Append("["); - Visit(expression.Right); - Sql.Append("]"); - return 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 VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression) - { - Debug.Assert(sqlUnaryExpression.TypeMapping is not null); - Debug.Assert(sqlUnaryExpression.Operand.TypeMapping is not null); - - switch (sqlUnaryExpression.OperatorType) - { - case ExpressionType.Convert: - { - // PostgreSQL supports the standard CAST(x AS y), but also a lighter x::y which we use - // where there's no precedence issues - switch (sqlUnaryExpression.Operand) - { - case SqlConstantExpression: - case SqlParameterExpression: - case SqlUnaryExpression { OperatorType: ExpressionType.Convert }: - case ColumnExpression: - case SqlFunctionExpression: - case ScalarSubqueryExpression: - var storeType = sqlUnaryExpression.TypeMapping.StoreType switch - { - "integer" => "int", - "timestamp with time zone" => "timestamptz", - "timestamp without time zone" => "timestamp", - var s => s - }; - - Visit(sqlUnaryExpression.Operand); - Sql.Append("::"); - Sql.Append(storeType); - return sqlUnaryExpression; - } - - break; - } - - // Bitwise complement on networking types - case ExpressionType.Not when - sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(IPAddress) - || sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof((IPAddress, int)) - || sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(PhysicalAddress): - Sql.Append("~"); - Visit(sqlUnaryExpression.Operand); - return sqlUnaryExpression; - - // 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); - } - - /// - /// 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 void GenerateSetOperationOperand(SetOperationBase setOperation, SelectExpression operand) - { - // PostgreSQL allows ORDER BY and LIMIT in set operation operands, but requires parentheses - if (operand.Orderings.Count > 0 || operand.Limit is not null) - { - Sql.AppendLine("("); - using (Sql.Indent()) - { - Visit(operand); - } - - Sql.AppendLine().Append(")"); - return; - } - - base.GenerateSetOperationOperand(setOperation, operand); - } - - /// - /// 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 VisitCollate(CollateExpression collateExpression) - { - Check.NotNull(collateExpression, nameof(collateExpression)); - - Visit(collateExpression.Operand); - - // In PG, collation names are regular identifiers which need to be quoted for case-sensitivity. - Sql - .Append(" COLLATE ") - .Append(_sqlGenerationHelper.DelimitIdentifier(collateExpression.Collation)); - - return collateExpression; - } - - /// - /// 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 TryGenerateWithoutWrappingSelect(SelectExpression selectExpression) - // PostgreSQL supports VALUES as a top-level statement - and directly under set operations. - // However, when on the left side of a set operation, we need the column coming out of VALUES to be named, so we need the wrapping - // SELECT for that. - => selectExpression.Tables is not [ValuesExpression] - && base.TryGenerateWithoutWrappingSelect(selectExpression); - - /// - /// 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 void GenerateSetOperation(SetOperationBase setOperation) - { - GenerateSetOperationOperand(setOperation, setOperation.Source1); - - Sql - .AppendLine() - .Append( - setOperation switch - { - ExceptExpression => "EXCEPT", - IntersectExpression => "INTERSECT", - UnionExpression => "UNION", - _ => throw new InvalidOperationException(CoreStrings.UnknownEntity("SetOperationType")) - }) - .AppendLine(setOperation.IsDistinct ? string.Empty : " ALL"); - - // For ValuesExpression, we can remove its wrapping SelectExpression but only if on the right side of a set operation, since on - // the left side we need the column name to be specified. - if (setOperation.Source2 is - { - Tables: [ValuesExpression valuesExpression], - Offset: null, - Limit: null, - IsDistinct: false, - Predicate: null, - Having: null, - Orderings.Count: 0, - GroupBy.Count: 0, - } rightSelectExpression - && rightSelectExpression.Projection.Count == valuesExpression.ColumnNames.Count - && rightSelectExpression.Projection.Select( - (pe, index) => pe.Expression is ColumnExpression column - && column.Name == valuesExpression.ColumnNames[index]) - .All(e => e)) - { - GenerateValues(valuesExpression); - } - else - { - GenerateSetOperationOperand(setOperation, setOperation.Source2); - } - } - - /// - /// 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 VisitValues(ValuesExpression valuesExpression) - { - base.VisitValues(valuesExpression); - - // PostgreSQL VALUES supports setting the projects column names: FROM (VALUES (1), (2)) AS v(foo) - Sql.Append("("); - - for (var i = 0; i < valuesExpression.ColumnNames.Count; i++) - { - if (i > 0) - { - Sql.Append(", "); - } - - Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i])); - } - - Sql.Append(")"); - - return valuesExpression; - } - - /// - /// 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 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. - var rowValues = valuesExpression.RowValues; - - Sql.Append("VALUES "); - - for (var i = 0; i < rowValues.Count; i++) - { - // TODO: Do we want newlines here? - if (i > 0) - { - Sql.Append(", "); - } - - Visit(valuesExpression.RowValues[i]); - } - } - - #region PostgreSQL-specific expression types - - /// - /// 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 Expression VisitArrayAll(PgAllExpression expression) - { - Visit(expression.Item); - - Sql - .Append(" ") - .Append( - expression.OperatorType switch - { - PgAllOperatorType.Like => "LIKE", - PgAllOperatorType.ILike => "ILIKE", - _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {expression.OperatorType}") - }) - .Append(" ALL ("); - - Visit(expression.Array); - - Sql.Append(")"); - - return 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 virtual Expression VisitArrayAny(PgAnyExpression expression) - { - Visit(expression.Item); - - Sql - .Append(" ") - .Append( - expression.OperatorType switch - { - PgAnyOperatorType.Equal => "=", - PgAnyOperatorType.Like => "LIKE", - PgAnyOperatorType.ILike => "ILIKE", - _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {expression.OperatorType}") - }) - .Append(" ANY ("); - - Visit(expression.Array); - - Sql.Append(")"); - - return expression; - } - - /// - /// Produces SQL array index expression (e.g. arr[1]). - /// - protected virtual Expression VisitArrayIndex(PgArrayIndexExpression expression) - { - var requiresParentheses = RequiresParentheses(expression, expression.Array); - - if (requiresParentheses) - { - Sql.Append("("); - } - - Visit(expression.Array); - - if (requiresParentheses) - { - Sql.Append(")"); - } - - Sql.Append("["); - Visit(expression.Index); - Sql.Append("]"); - return expression; - } - - /// - /// Produces SQL array slice expression (e.g. arr[1:2]). - /// - protected virtual Expression VisitArraySlice(PgArraySliceExpression expression) - { - var requiresParentheses = RequiresParentheses(expression, expression.Array); - - if (requiresParentheses) - { - Sql.Append("("); - } - - Visit(expression.Array); - - if (requiresParentheses) - { - Sql.Append(")"); - } - - Sql.Append("["); - Visit(expression.LowerBound); - Sql.Append(":"); - Visit(expression.UpperBound); - Sql.Append("]"); - return expression; - } - - /// - /// Produces SQL for PostgreSQL regex matching. - /// - /// - /// See: http://www.postgresql.org/docs/current/static/functions-matching.html - /// - protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression, bool negated = false) - { - var options = expression.Options; - - Visit(expression.Match); - - if (options.HasFlag(RegexOptions.IgnoreCase)) - { - Sql.Append(negated ? " !~* " : " ~* "); - options &= ~RegexOptions.IgnoreCase; - } - else - { - Sql.Append(negated ? " !~ " : " ~ "); - } - - // PG regexps are single-line by default - if (options == RegexOptions.Singleline) - { - Visit(expression.Pattern); - return expression; - } - - var constantPattern = (expression.Pattern as SqlConstantExpression)?.Value as string; - - if (constantPattern is null) - { - Sql.Append("("); - } - - Sql.Append("'(?"); - - if (options.HasFlag(RegexOptions.Multiline)) - { - Sql.Append("n"); - } - else if (!options.HasFlag(RegexOptions.Singleline)) - { - // In .NET's default mode, . doesn't match newlines but in PostgreSQL it does. - Sql.Append("p"); - } - - if (options.HasFlag(RegexOptions.IgnorePatternWhitespace)) - { - Sql.Append("x"); - } - - Sql.Append(")"); - - if (constantPattern is null) - { - Sql.Append("' || "); - Visit(expression.Pattern); - Sql.Append(")"); - } - else - { - Sql.Append(constantPattern.Replace("'", "''")); - Sql.Append("'"); - } - - return 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 virtual Expression VisitRowValue(PgRowValueExpression rowValueExpression) - { - Sql.Append("("); - - var values = rowValueExpression.Values; - var count = values.Count; - for (var i = 0; i < count; i++) - { - Visit(values[i]); - - if (i < count - 1) - { - Sql.Append(", "); - } - } - - Sql.Append(")"); - - return rowValueExpression; - } - - /// - /// 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 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) - { - Sql.Append(" ESCAPE "); - Visit(likeExpression.EscapeChar); - } - - return likeExpression; - } - - /// - /// 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 VisitJsonScalar(JsonScalarExpression jsonScalarExpression) - { - // TODO: Stop producing empty JsonScalarExpressions, #30768 - var segmentsPath = jsonScalarExpression.Path; - if (segmentsPath.Count == 0) - { - Visit(jsonScalarExpression.Json); - return jsonScalarExpression; - } - - var path = ConvertJsonPathSegments(segmentsPath); - - switch (jsonScalarExpression.TypeMapping) - { - // Nested JSON structural type. We want the json/jsonb fragment in this case (not text), so we can perform further JSON - // operations on it. - case NpgsqlStructuralJsonTypeMapping: - GenerateJsonPath(jsonScalarExpression.Json, returnsText: false, path); - break; - - // A scalar to be extracted out as JSON (and not as e.g. text/int). - // This happens for scalar collection mapped to JSON (either because it's nested within a JSON document, - // or because the user explicitly opted for this rather than the default PG array mapping), or when we copy a scalar JSON - // property from one document to another via ExecuteUpdate. - case NpgsqlJsonTypeMapping typeMapping: - Check.DebugAssert(typeMapping.ElementTypeMapping is not null); - GenerateJsonPath(jsonScalarExpression.Json, returnsText: false, path); - break; - - // No need to cast the output when we expect a string anyway - case StringTypeMapping: - GenerateJsonPath(jsonScalarExpression.Json, returnsText: true, path); - break; - - // bytea requires special handling, since we encode the binary data as base64 inside the JSON, but that requires a special - // conversion function to be extracted out to a PG bytea. - case NpgsqlByteArrayTypeMapping: - Sql.Append("decode("); - GenerateJsonPath(jsonScalarExpression.Json, returnsText: true, path); - Sql.Append(", 'base64')"); - break; - - // We should never have an NpgsqlArrayTypeMapping within a JSON document; scalar collections should be represented as an - // NpgsqlJsonTypeMapping with the appropriate ElementTypeMapping, just like in other providers. - case NpgsqlArrayTypeMapping: - throw new UnreachableException(); - - default: - Sql.Append("CAST("); - GenerateJsonPath(jsonScalarExpression.Json, returnsText: true, path); - Sql.Append(" AS "); - Sql.Append(jsonScalarExpression.TypeMapping!.StoreType); - Sql.Append(")"); - break; - } - - return jsonScalarExpression; - } - - /// - /// Visits the children of an . - /// - /// The expression. - /// - /// An . - /// - protected virtual Expression VisitJsonPathTraversal(PgJsonTraversalExpression expression) - { - GenerateJsonPath(expression.Expression, expression.ReturnsText, expression.Path); - return expression; - } - - private void GenerateJsonPath(SqlExpression expression, bool returnsText, IReadOnlyList path) - { - Visit(expression); - - if (path.Count == 1) - { - Sql.Append(returnsText ? " ->> " : " -> "); - Visit(path[0]); - return; - } - - // Multiple path components - Sql.Append(returnsText ? " #>> " : " #> "); - - GenerateJsonPath(path); - } - - private void GenerateJsonPath(IReadOnlyList path) - { - // Use simplified array literal syntax if all path components are constants for cleaner SQL - if (path.All(p => p is SqlConstantExpression { Value: var pathSegment } - && (pathSegment is not string s || s.All(char.IsAsciiLetterOrDigit)))) - { - Sql - .Append("'{") - .Append(string.Join(",", path.Select(p => ((SqlConstantExpression)p).Value))) - .Append("}'"); - } - else - { - Sql.Append("ARRAY["); - for (var i = 0; i < path.Count; i++) - { - Visit(path[i]); - if (i < path.Count - 1) - { - Sql.Append(","); - } - } - - Sql.Append("]::text[]"); - } - } - - /// - /// Converts the standard EF to an - /// (the EF built-in and don't support non-constant - /// property names, but we do via the Npgsql-specific JSON DOM support). - /// - private IReadOnlyList ConvertJsonPathSegments(IReadOnlyList path) - => path - .Select( - s => s switch - { - { PropertyName: string propertyName } - => new SqlConstantExpression(propertyName, _textTypeMapping ??= _typeMappingSource.FindMapping(typeof(string))), - { ArrayIndex: SqlExpression arrayIndex } => arrayIndex, - _ => throw new UnreachableException() - }) - .ToList(); - - /// - /// 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 Expression VisitPgTableValuedFunctionExpression(PgTableValuedFunctionExpression tableValuedFunctionExpression) - { - // PostgresTableValuedFunctionExpression extends the standard TableValuedFunctionExpression, adding the possibility to specify - // column names and types at the end, as well as an optional WITH ORDINALITY to project an index out. - - // Note that PostgreSQL doesn't support specifying both a column definition (with type) *and* WITH ORDINALITY; but it does allow - // wrapping the function invocation and column definition inside ROWS FROM, and placing the table alias and WITH ORDINALITY outside. - // We take care of that here. - if (tableValuedFunctionExpression is - { - WithOrdinality: true, - ColumnInfos: { } columnInfos - } - && columnInfos.Any(ci => ci.TypeMapping is not null)) - { - Sql.Append("ROWS FROM ("); - - Sql.Append(tableValuedFunctionExpression.Name).Append("("); - GenerateList(tableValuedFunctionExpression.Arguments, e => Visit(e)); - Sql.Append(") AS "); - - GenerateColumnDefinition(); - - Sql.Append(") WITH ORDINALITY AS ").Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Alias)); - } - else - { - Sql.Append(tableValuedFunctionExpression.Name).Append("("); - GenerateList(tableValuedFunctionExpression.Arguments, e => Visit(e)); - Sql.Append(")"); - - if (tableValuedFunctionExpression.WithOrdinality) - { - Sql.Append(" WITH ORDINALITY"); - } - - Sql.Append(AliasSeparator).Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Alias)); - - if (tableValuedFunctionExpression.ColumnInfos is not null) - { - GenerateColumnDefinition(); - } - } - - return tableValuedFunctionExpression; - - void GenerateColumnDefinition() - { - Sql.Append("("); - - if (tableValuedFunctionExpression.ColumnInfos is [var singleColumnInfo]) - { - GenerateColumnInfo(singleColumnInfo); - } - else - { - Sql.AppendLine(); - using var _ = Sql.Indent(); - - for (var i = 0; i < tableValuedFunctionExpression.ColumnInfos.Count; i++) - { - var columnInfo = tableValuedFunctionExpression.ColumnInfos[i]; - - if (i > 0) - { - Sql.AppendLine(","); - } - - GenerateColumnInfo(columnInfo); - } - - Sql.AppendLine(); - } - - Sql.Append(")"); - - void GenerateColumnInfo(PgTableValuedFunctionExpression.ColumnInfo columnInfo) - { - Sql.Append(_sqlGenerationHelper.DelimitIdentifier(columnInfo.Name)); - - if (columnInfo.TypeMapping is not null) - { - Sql.Append(" ").Append(columnInfo.TypeMapping.StoreType); - } - } - } - } - - /// - /// Visits the children of a . - /// - /// The expression. - /// - /// An . - /// - protected virtual Expression VisitUnknownBinary(PgUnknownBinaryExpression unknownBinaryExpression) - { - Check.NotNull(unknownBinaryExpression, nameof(unknownBinaryExpression)); - - var requiresParentheses = RequiresParentheses(unknownBinaryExpression, unknownBinaryExpression.Left); - - if (requiresParentheses) - { - Sql.Append("("); - } - - Visit(unknownBinaryExpression.Left); - - if (requiresParentheses) - { - Sql.Append(")"); - } - - Sql - .Append(" ") - .Append(unknownBinaryExpression.Operator) - .Append(" "); - - requiresParentheses = RequiresParentheses(unknownBinaryExpression, unknownBinaryExpression.Right); - - if (requiresParentheses) - { - Sql.Append("("); - } - - Visit(unknownBinaryExpression.Right); - - if (requiresParentheses) - { - Sql.Append(")"); - } - - return unknownBinaryExpression; - } - - /// - /// Visits the children of a . - /// - /// The expression. - /// - /// An . - /// - protected virtual Expression VisitPgFunction(PgFunctionExpression e) - { - Check.NotNull(e, nameof(e)); - - if (e.IsBuiltIn) - { - Sql.Append(e.Name); - } - else - { - if (!string.IsNullOrEmpty(e.Schema)) - { - Sql - .Append(_sqlGenerationHelper.DelimitIdentifier(e.Schema)) - .Append("."); - } - - // TODO: Quote user-defined function names with upper-case (also for regular SqlFunctionExpression) - - Sql.Append(_sqlGenerationHelper.DelimitIdentifier(e.Name)); - } - - Sql.Append("("); - - if (e.IsAggregateDistinct) - { - Sql.Append("DISTINCT "); - } - - for (var i = 0; i < e.Arguments.Count; i++) - { - if (i < e.ArgumentNames.Count && e.ArgumentNames[i] is { } argumentName) - { - Sql - .Append(argumentName) - .Append(" => "); - } - - Visit(e.Arguments[i]); - - if (i < e.Arguments.Count - 1) - { - Sql.Append( - i < e.ArgumentSeparators.Count && e.ArgumentSeparators[i] is not null - ? $" {e.ArgumentSeparators[i]} " - : ", "); - } - } - - if (e.AggregateOrderings.Count > 0) - { - Sql.Append(" ORDER BY "); - - for (var i = 0; i < e.AggregateOrderings.Count; i++) - { - if (i > 0) - { - Sql.Append(", "); - } - - Visit(e.AggregateOrderings[i]); - } - } - - Sql.Append(")"); - - if (e.AggregatePredicate is not null) - { - Sql.Append(" FILTER (WHERE "); - - Visit(e.AggregatePredicate); - - Sql.Append(")"); - } - - return e; - } - - #endregion PostgreSQL-specific expression types - - /// - /// 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 RequiresParentheses(SqlExpression outerExpression, SqlExpression innerExpression) - { - switch (innerExpression) - { - // PG doesn't support ~-, -~, ~~, -- so we add parentheses - case SqlUnaryExpression innerUnary when outerExpression is SqlUnaryExpression outerUnary - && (innerUnary.OperatorType is ExpressionType.Negate - || innerUnary.OperatorType is ExpressionType.Not && innerUnary.Type != typeof(bool)) - && (outerUnary.OperatorType is ExpressionType.Negate - || outerUnary.OperatorType is ExpressionType.Not && outerUnary.Type != typeof(bool)): - return true; - - // Copy paste of QuerySqlGenerator.RequiresParentheses for SqlBinaryExpression - case PgBinaryExpression innerBinary: - { - // If the provider defined precedence for the two expression, use that - if (TryGetOperatorInfo(outerExpression, out var outerPrecedence, out var isOuterAssociative) - && TryGetOperatorInfo(innerExpression, out var innerPrecedence, out _)) - { - return outerPrecedence.CompareTo(innerPrecedence) switch - { - > 0 => true, - < 0 => false, - - // 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) - _ => outerExpression is not PgBinaryExpression outerBinary - || outerBinary.OperatorType != innerBinary.OperatorType - || !isOuterAssociative - // Arithmetic operators on floating points aren't associative, because of rounding errors. - || outerExpression.Type == typeof(float) - || outerExpression.Type == typeof(double) - || innerExpression.Type == typeof(float) - || innerExpression.Type == typeof(double) - }; - } - - // Otherwise always parenthesize for safety - return true; - } - - case PgUnknownBinaryExpression: - return true; - - default: - return base.RequiresParentheses(outerExpression, innerExpression); - } - } - - /// - /// 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 TryGetOperatorInfo(SqlExpression expression, out int precedence, out bool isAssociative) - { - // See https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-PRECEDENCE - (precedence, isAssociative) = expression switch - { - // TODO: Exponent => 1300 - - SqlBinaryExpression sqlBinaryExpression => sqlBinaryExpression.OperatorType switch - { - // Multiplication, division, modulo - ExpressionType.Multiply => (1200, true), - ExpressionType.Divide => (1200, false), - ExpressionType.Modulo => (1200, false), - - // Addition, subtraction (binary) - ExpressionType.Add => (1100, true), - ExpressionType.Subtract => (1100, false), - - // All other native and user-defined operators => 1000 - ExpressionType.LeftShift => (1000, true), - ExpressionType.RightShift => (1000, true), - ExpressionType.And when sqlBinaryExpression.Type != typeof(bool) => (1000, true), - ExpressionType.Or when sqlBinaryExpression.Type != typeof(bool) => (1000, true), - - // Comparison operators - ExpressionType.Equal => (800, false), - ExpressionType.NotEqual => (800, false), - ExpressionType.LessThan => (800, false), - ExpressionType.LessThanOrEqual => (800, false), - ExpressionType.GreaterThan => (800, false), - ExpressionType.GreaterThanOrEqual => (800, false), - - // Logical operators - ExpressionType.AndAlso => (500, true), - ExpressionType.OrElse => (500, true), - ExpressionType.And when sqlBinaryExpression.Type == typeof(bool) => (500, true), - ExpressionType.Or when sqlBinaryExpression.Type == typeof(bool) => (500, true), - - _ => default, - }, - - SqlUnaryExpression sqlUnaryExpression => sqlUnaryExpression.OperatorType switch - { - ExpressionType.Convert => (1600, false), - ExpressionType.Negate => (1400, false), - ExpressionType.Not when sqlUnaryExpression.Type != typeof(bool) => (1000, false), - ExpressionType.Equal => (700, false), // IS NULL - ExpressionType.NotEqual => (700, false), // IS NOT NULL - ExpressionType.Not when sqlUnaryExpression.Type == typeof(bool) => (600, false), - - _ => default, - }, - - // There's an "any other operator" category in the PG operator precedence table, we assign that a numeric value of 1000. - // TODO: Some operators here may be associative - PgBinaryExpression => (1000, false), - - CollateExpression => (1000, false), - AtTimeZoneExpression => (1100, false), - InExpression => (900, false), - PgJsonTraversalExpression => (1000, false), - PgArrayIndexExpression => (1500, false), - PgAllExpression or PgAnyExpression => (800, false), - LikeExpression or PgILikeExpression or PgRegexMatchExpression => (900, false), - - _ => default, - }; - - return precedence != default; - } - - private void GenerateList( - IReadOnlyList items, - Action generationAction, - Action? joinAction = null) - { - joinAction ??= (isb => isb.Append(", ")); - - for (var i = 0; i < items.Count; i++) - { - if (i > 0) - { - joinAction(Sql); - } - - generationAction(items[i]); - } - } - - private sealed class OuterReferenceFindingExpressionVisitor(TableExpression mainTable) : ExpressionVisitor - { - private bool _containsReference; - - public bool ContainsReferenceToMainTable(SqlExpression sqlExpression) - { - _containsReference = false; - - Visit(sqlExpression); - - return _containsReference; - } - - [return: NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - { - if (_containsReference) - { - return expression; - } - - if (expression is ColumnExpression { TableAlias: var tableAlias } - && tableAlias == mainTable.Alias) - { - _containsReference = true; - - return expression; - } - - return base.Visit(expression); - } - } -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGeneratorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGeneratorFactory.cs deleted file mode 100644 index 5126ed903c..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGeneratorFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; - -/// -/// The default factory for Npgsql-specific query SQL generators. -/// -public class NpgsqlQuerySqlGeneratorFactory : IQuerySqlGeneratorFactory -{ - private readonly QuerySqlGeneratorDependencies _dependencies; - private readonly IRelationalTypeMappingSource _typeMappingSource; - 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 NpgsqlQuerySqlGeneratorFactory( - QuerySqlGeneratorDependencies dependencies, - IRelationalTypeMappingSource typeMappingSource, - INpgsqlSingletonOptions npgsqlSingletonOptions) - { - _dependencies = dependencies; - _typeMappingSource = typeMappingSource; - _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. - /// - public virtual QuerySqlGenerator Create() - => new NpgsqlQuerySqlGenerator( - _dependencies, - _typeMappingSource, - _npgsqlSingletonOptions.ReverseNullOrderingEnabled, - _npgsqlSingletonOptions.PostgresVersion); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs deleted file mode 100644 index 0bb5e96d65..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs +++ /dev/null @@ -1,60 +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 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 - /// 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 NpgsqlQueryTranslationPostprocessor( - QueryTranslationPostprocessorDependencies dependencies, - RelationalQueryTranslationPostprocessorDependencies relationalDependencies, - RelationalQueryCompilationContext queryCompilationContext) - : base(dependencies, relationalDependencies, queryCompilationContext) - { - } - - /// - /// 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 Expression Process(Expression query) - { - var result = base.Process(query); - - result = new NpgsqlUnnestPostprocessor().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 deleted file mode 100644 index e31cc18beb..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessorFactory.cs +++ /dev/null @@ -1,41 +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 NpgsqlQueryTranslationPostprocessorFactory : IQueryTranslationPostprocessorFactory -{ - /// - /// 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 NpgsqlQueryTranslationPostprocessorFactory( - QueryTranslationPostprocessorDependencies dependencies, - RelationalQueryTranslationPostprocessorDependencies relationalDependencies) - { - Dependencies = dependencies; - RelationalDependencies = relationalDependencies; - } - - /// - /// Dependencies for this service. - /// - protected virtual QueryTranslationPostprocessorDependencies Dependencies { get; } - - /// - /// Relational provider-specific dependencies for this service. - /// - protected virtual RelationalQueryTranslationPostprocessorDependencies RelationalDependencies { get; } - - /// - public virtual QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext) - => new NpgsqlQueryTranslationPostprocessor( - Dependencies, - RelationalDependencies, - (RelationalQueryCompilationContext)queryCompilationContext); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs deleted file mode 100644 index 9f0806a540..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs +++ /dev/null @@ -1,1481 +0,0 @@ -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; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -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 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), [typeof(DbFunctions), typeof(string), typeof(string)])!; - - // ReSharper disable once InconsistentNaming - private static readonly MethodInfo ILike2MethodInfo - = typeof(NpgsqlDbFunctionsExtensions).GetRuntimeMethod( - nameof(NpgsqlDbFunctionsExtensions.ILike), [typeof(DbFunctions), typeof(string), typeof(string)])!; - - private static readonly MethodInfo MatchesLQuery = - typeof(LTree).GetRuntimeMethod(nameof(LTree.MatchesLQuery), [typeof(string)])!; - - #endregion - - /// - /// 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 NpgsqlQueryableMethodTranslatingExpressionVisitor( - QueryableMethodTranslatingExpressionVisitorDependencies dependencies, - RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, - RelationalQueryCompilationContext queryCompilationContext, - INpgsqlSingletonOptions npgsqlSingletonOptions) - : base(dependencies, relationalDependencies, queryCompilationContext) - { - _queryCompilationContext = queryCompilationContext; - _typeMappingSource = (NpgsqlTypeMappingSource)relationalDependencies.TypeMappingSource; - _sqlExpressionFactory = (NpgsqlSqlExpressionFactory)relationalDependencies.SqlExpressionFactory; - _isRedshift = npgsqlSingletonOptions.UseRedshift; - } - - /// - /// 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 NpgsqlQueryableMethodTranslatingExpressionVisitor(NpgsqlQueryableMethodTranslatingExpressionVisitor parentVisitor) - : base(parentVisitor) - { - _queryCompilationContext = parentVisitor._queryCompilationContext; - _typeMappingSource = parentVisitor._typeMappingSource; - _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; - _isRedshift = parentVisitor._isRedshift; - } - - /// - /// 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 QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() - => new NpgsqlQueryableMethodTranslatingExpressionVisitor(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. - /// - 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; - - // If this is a collection property, get the element's nullability out of metadata. Otherwise, this is a parameter property, in - // which case we only have the CLR type (note that we cannot produce different SQLs based on the nullability of an *element* in - // a parameter collection - our caching mechanism only supports varying by the nullability of the parameter itself (i.e. the - // collection). - // TODO: if property is non-null, GetElementType() should never be null, but we have #31469 for shadow properties - var isElementNullable = property?.GetElementType() is null - ? elementClrType.IsNullableType() - : property.GetElementType()!.IsNullable; - - // We support two kinds of primitive collections: the standard one with PostgreSQL arrays (where we use the unnest function), and - // a special case for geometry collections, where we use - SelectExpression selectExpression; - - // 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 - // inferred". -#pragma warning disable EF1001 // SelectExpression constructors are pubternal - switch (sqlExpression.TypeMapping) - { - case { StoreTypeNameBase: "geometry" or "geography" }: - { - // TODO: For geometry collection support (not yet supported), see #2850. - selectExpression = new SelectExpression( - [new TableValuedFunctionExpression(tableAlias, "ST_Dump", [sqlExpression])], - new ColumnExpression("geom", tableAlias, elementClrType.UnwrapNullableType(), elementTypeMapping, isElementNullable), - identifier: [], // TODO - _queryCompilationContext.SqlAliasManager); - break; - } - - // Scalar/primitive collection mapped to a PostgreSQL array (typical and default case) - case NpgsqlArrayTypeMapping or NpgsqlMultirangeTypeMapping or null: - { - // Note that for unnest we have a special expression type extending TableValuedFunctionExpression, adding the ability to provide - // an explicit column name for its output (SELECT * FROM unnest(array) AS f(foo)). - // This is necessary since when the column name isn't explicitly specified, it is automatically identical to the table alias - // (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. 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")], - new ColumnExpression( - "value", - tableAlias, - elementClrType.UnwrapNullableType(), - elementTypeMapping, - isElementNullable), - identifier: [(ordinalityColumn, ordinalityComparer)], - _queryCompilationContext.SqlAliasManager); - - selectExpression.AppendOrdering(new OrderingExpression(ordinalityColumn, ascending: true)); - break; - } - - // Scalar/primitive collection mapped to a JSON array, like in other providers. - // Happens for scalar collections nested within JSON documents, or if the user explicitly mapped to JSON instead of - // the default PG array. - // Translate to SELECT element::int FROM jsonb_array_elements_text(...) WITH ORDINALITY - case NpgsqlJsonTypeMapping { ElementTypeMapping: not null, StoreType: var storeType }: - { - var (ordinalityColumn, ordinalityComparer) = GenerateOrdinalityIdentifier(tableAlias); - - SqlExpression elementProjection = new ColumnExpression( - "element", - tableAlias, - typeof(string), - _typeMappingSource.FindMapping(typeof(string)), - isElementNullable); - - // If the projected type is anything other than a text, apply a cast (jsonb_array_elements_text returns text) - if (!elementTypeMapping!.StoreType.Equals("text", StringComparison.OrdinalIgnoreCase)) - { - elementProjection = _sqlExpressionFactory.Convert( - elementProjection, - elementClrType.UnwrapNullableType(), - elementTypeMapping); - } - - selectExpression = new SelectExpression( - [ - new PgTableValuedFunctionExpression( - tableAlias, - storeType switch - { - "jsonb" => "jsonb_array_elements_text", - "json" => "json_array_elements_text", - _ => throw new UnreachableException() - }, - [sqlExpression], - columnInfos: [new("element")], - withOrdinality: true) - ], - elementProjection, - identifier: [(ordinalityColumn, ordinalityComparer)], - _queryCompilationContext.SqlAliasManager); - - selectExpression.AppendOrdering(new OrderingExpression(ordinalityColumn, ascending: true)); - break; - } - - default: - throw new UnreachableException(); - } -#pragma warning restore EF1001 // SelectExpression constructors are pubternal - - Expression shaperExpression = new ProjectionBindingExpression( - selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); - - if (elementClrType != shaperExpression.Type) - { - Check.DebugAssert( - elementClrType.MakeNullable() == shaperExpression.Type, - "expression.Type must be nullable of targetType"); - - shaperExpression = Expression.Convert(shaperExpression, elementClrType); - } - - return new ShapedQueryExpression(selectExpression, shaperExpression); - } - - /// - /// 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 TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression) - { - // 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 = - _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 - { - { TypeMapping.StoreType: "jsonb" } => "jsonb_to_recordset", - { TypeMapping.StoreType: "json" } => "json_to_recordset", - { TypeMapping: null } => throw new UnreachableException("Missing type mapping on JSON column"), - - _ => throw new UnreachableException() - }; - - var jsonTypeMapping = jsonQueryExpression.JsonColumn.TypeMapping!; - Check.DebugAssert(jsonTypeMapping is NpgsqlStructuralJsonTypeMapping, "JSON column has a non-JSON mapping"); - - // We now add all of projected entity's the properties and navigations into the jsonb_to_recordset's AS clause, which defines the - // names and types of columns to come out of the JSON fragments. - var columnInfos = new List(); - - // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys - foreach (var property in jsonQueryExpression.StructuralType.GetPropertiesInHierarchy()) - { - if (property.GetJsonPropertyName() is string jsonPropertyName) - { - columnInfos.Add( - new PgTableValuedFunctionExpression.ColumnInfo - { - Name = jsonPropertyName, TypeMapping = property.GetRelationalTypeMapping() - }); - } - } - - switch (jsonQueryExpression.StructuralType) - { - case IEntityType entityType: - foreach (var navigation in entityType.GetNavigationsInHierarchy() - .Where(n => n.ForeignKey.IsOwnership - && n.TargetEntityType.IsMappedToJson() - && n.ForeignKey.PrincipalToDependent == n)) - { - var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName(); - Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}"); - - columnInfos.Add( - new PgTableValuedFunctionExpression.ColumnInfo { Name = jsonNavigationName, TypeMapping = jsonTypeMapping }); - } - - break; - - case IComplexType complexType: - foreach (var complexProperty in complexType.GetComplexProperties()) - { - var jsonPropertyName = complexProperty.ComplexType.GetJsonPropertyName(); - Check.DebugAssert(jsonPropertyName is not null, $"No JSON property name for complex property {complexProperty.Name}"); - - columnInfos.Add( - new PgTableValuedFunctionExpression.ColumnInfo { Name = jsonPropertyName, TypeMapping = jsonTypeMapping }); - } - - break; - - default: - throw new UnreachableException(); - } - - // json_to_recordset requires the nested JSON document - it does not accept a path within a containing JSON document (like SQL - // Server OPENJSON or SQLite json_each). So we wrap json_to_recordset around a JsonScalarExpression which will extract the nested - // document. - var jsonScalarExpression = new JsonScalarExpression( - jsonQueryExpression.JsonColumn, jsonQueryExpression.Path, typeof(string), jsonTypeMapping, jsonQueryExpression.IsNullable); - - // Construct the json_to_recordset around the JsonScalarExpression, and wrap it in a SelectExpression - var jsonToRecordSetExpression = new PgTableValuedFunctionExpression( - tableAlias, functionName, [jsonScalarExpression], columnInfos, withOrdinality: true); - -#pragma warning disable EF1001 // SelectExpression constructors are currently internal - var selectExpression = CreateSelect( - jsonQueryExpression, - jsonToRecordSetExpression, - "ordinality", - typeof(int), - _typeMappingSource.FindMapping(typeof(int))!); -#pragma warning restore EF1001 // Internal EF Core API usage. - - return new ShapedQueryExpression( - selectExpression, - new RelationalStructuralTypeShaperExpression( - jsonQueryExpression.StructuralType, - new ProjectionBindingExpression( - selectExpression, - new ProjectionMember(), - typeof(ValueBuffer)), - false)); - } - - /// - /// 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? TranslateAll(ShapedQueryExpression source, LambdaExpression predicate) - { - if ((source.TryExtractArray(out var array, ignoreOrderings: true) - || source.TryConvertToArray(out array, ignoreOrderings: true)) - && source.QueryExpression is SelectExpression { Tables: [{ Alias: var tableAlias }] } - && TranslateLambdaExpression(source, predicate) is { } translatedPredicate) - { - switch (translatedPredicate) - { - // Pattern match for: new[] { "a", "b", "c" }.All(p => EF.Functions.Like(e.SomeText, p)), - // which we translate to WHERE s.""SomeText"" LIKE ALL (ARRAY['a','b','c']) - case LikeExpression - { - Match: var match, - Pattern: ColumnExpression pattern, - EscapeChar: SqlConstantExpression { Value: "" } - } - when pattern.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.All(match, array, PgAllOperatorType.Like)); - } - - // Pattern match for: new[] { "a", "b", "c" }.All(p => EF.Functions.Like(e.SomeText, p)), - // which we translate to WHERE s.""SomeText"" LIKE ALL (ARRAY['a','b','c']) - case PgILikeExpression - { - Match: var match, - Pattern: ColumnExpression pattern, - EscapeChar: SqlConstantExpression { Value: "" } - } - when pattern.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.All(match, array, PgAllOperatorType.ILike)); - } - - // Pattern match for: e.SomeArray.All(p => ints.Contains(p)) over non-column, - // using array containment (<@) - case PgAnyExpression - { - Item: ColumnExpression sourceColumn, - Array: var otherArray - } - when sourceColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(array, otherArray)); - } - - // Pattern match for: new[] { 4, 5 }.All(p => e.SomeArray.Contains(p)) over column, - // using array containment (<@) - case PgBinaryExpression - { - OperatorType: PgExpressionType.Contains, - Left: var otherArray, - Right: PgNewArrayExpression { Expressions: [ColumnExpression sourceColumn] } - } - when sourceColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.ContainedBy(array, otherArray)); - } - } - } - - return base.TranslateAll(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 ShapedQueryExpression? TranslateAny(ShapedQueryExpression source, LambdaExpression? predicate) - { - if (source.QueryExpression is SelectExpression { Tables: [{ Alias: var tableAlias }] }) - { - // Pattern match: x.JsonArray.Any() - // Translation: jsonb_array_length(x.json_array) > 0 instead of EXISTS (SELECT 1 FROM FROM jsonb_array_elements(x.Array)) - if (predicate is null && source.TryExtractJsonArray(out var jsonArray, out _, out _, ignoreOrderings: true)) - { - return BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.GreaterThan( - _sqlExpressionFactory.Function( - jsonArray.TypeMapping!.StoreType switch - { - "jsonb" => "jsonb_array_length", - "json" => "json_array_length", - _ => throw new UnreachableException() - }, - [jsonArray], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)), - _sqlExpressionFactory.Constant(0))); - } - - if (source.TryExtractArray(out var array, ignoreOrderings: true) - || source.TryConvertToArray(out array, ignoreOrderings: true)) - { - // Pattern match: x.Array.Any() - // Translation: cardinality(x.array) > 0 instead of EXISTS (SELECT 1 FROM FROM unnest(x.Array)) - if (predicate is null) - { - return BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.GreaterThan( - _sqlExpressionFactory.Function( - "cardinality", - [array], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)), - _sqlExpressionFactory.Constant(0))); - } - - if (TranslateLambdaExpression(source, predicate) is not SqlExpression translatedPredicate) - { - return null; - } - - switch (translatedPredicate) - { - // Pattern match: new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)) - // Translation: s.SomeText LIKE ANY (ARRAY['a','b','c']) - case LikeExpression - { - Match: var match, - Pattern: ColumnExpression pattern, - EscapeChar: SqlConstantExpression { Value: "" } - } - when pattern.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, _sqlExpressionFactory.Any(match, array, PgAnyOperatorType.Like)); - } - - // Pattern match: new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p)) - // Translation: s.SomeText LIKE ANY (ARRAY['a','b','c']) - case PgILikeExpression - { - Match: var match, - Pattern: ColumnExpression pattern, - EscapeChar: SqlConstantExpression { Value: "" } - } - when pattern.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, _sqlExpressionFactory.Any(match, array, PgAnyOperatorType.ILike)); - } - - // Array overlap over non-column - // Pattern match: e.SomeArray.Any(p => ints.Contains(p)) - // Translation: @ints && s.SomeArray - case PgAnyExpression - { - Item: ColumnExpression sourceColumn, - Array: var otherArray - } - when sourceColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(array, otherArray)); - } - - // Array overlap over column - // Pattern match: new[] { 4, 5 }.Any(p => e.SomeArray.Contains(p)) - // Translation: s.SomeArray && ARRAY[4, 5] - case PgBinaryExpression - { - OperatorType: PgExpressionType.Contains, - Left: var otherArray, - Right: PgNewArrayExpression { Expressions: [ColumnExpression sourceColumn] } - } - when sourceColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(array, otherArray)); - } - - #region LTree translations - - // Pattern match: new[] { "q1", "q2" }.Any(q => e.SomeLTree.MatchesLQuery(q)) - // Translation: s.SomeLTree ? ARRAY['q1','q2'] - case PgBinaryExpression - { - OperatorType: PgExpressionType.LTreeMatches, - Left: var ltree, - Right: SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression lqueryColumn } - } - when lqueryColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - new PgBinaryExpression( - PgExpressionType.LTreeMatchesAny, - ltree, - _sqlExpressionFactory.ApplyTypeMapping(array, _typeMappingSource.FindMapping("lquery[]")), - typeof(bool), - typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); - } - - // Pattern match: new[] { "t1", "t2" }.Any(t => t.IsAncestorOf(e.SomeLTree)) - // Translation: ARRAY['t1','t2'] @> s.SomeLTree - // Pattern match: new[] { "t1", "t2" }.Any(t => t.IsDescendantOf(e.SomeLTree)) - // Translation: ARRAY['t1','t2'] <@ s.SomeLTree - case PgBinaryExpression - { - OperatorType: (PgExpressionType.Contains or PgExpressionType.ContainedBy) and var operatorType, - Left: ColumnExpression ltreeColumn, - // Contains/ContainedBy can happen for non-LTree types too, so check that - Right: { TypeMapping: NpgsqlLTreeTypeMapping } ltree - } - when ltreeColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - new PgBinaryExpression( - operatorType, - _sqlExpressionFactory.ApplyDefaultTypeMapping(array), - ltree, - typeof(bool), - typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); - } - - // Pattern match: new[] { "t1", "t2" }.Any(t => t.MatchesLQuery(lquery)) - // Translation: ARRAY['t1','t2'] ~ lquery - // Pattern match: new[] { "t1", "t2" }.Any(t => t.MatchesLTxtQuery(ltxtquery)) - // Translation: ARRAY['t1','t2'] @ ltxtquery - case PgBinaryExpression - { - OperatorType: PgExpressionType.LTreeMatches, - Left: ColumnExpression ltreeColumn, - Right: var lquery - } - when ltreeColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - new PgBinaryExpression( - PgExpressionType.LTreeMatches, - _sqlExpressionFactory.ApplyDefaultTypeMapping(array), - lquery, - typeof(bool), - typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); - } - - // Any within Any (i.e. intersection) - // Pattern match: ltrees.Any(t => lqueries.Any(q => t.MatchesLQuery(q))) - // Translate: ltrees ? lqueries - case PgBinaryExpression - { - OperatorType: PgExpressionType.LTreeMatchesAny, - Left: ColumnExpression ltreeColumn, - Right: var lqueries - } - when ltreeColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - new PgBinaryExpression( - PgExpressionType.LTreeMatchesAny, - _sqlExpressionFactory.ApplyDefaultTypeMapping(array), - lqueries, - typeof(bool), - typeMapping: _typeMappingSource.FindMapping(typeof(bool)))); - } - - #endregion LTree translations - } - } - } - - // Pattern match: x.Array1.Intersect(x.Array2).Any() - // Translation: x.Array1 && x.Array2 - if (predicate is null - && source.QueryExpression is SelectExpression - { - Tables: - [ - IntersectExpression - { - Source1: - { - Tables: [PgUnnestExpression { Array: var array1 }], - Predicate: null, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - }, - Source2: - { - Tables: [PgUnnestExpression { Array: var array2 }], - Predicate: null, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - } - } - ], - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null - }) - { - return BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Overlaps(array1, array2)); - } - - return base.TranslateAny(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 ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item) - { - switch (source) - { - // Contains over array, e.g. x = ANY(y.Array), or y.Array @> ARRAY[x] - // 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. - case var _ when source.TryExtractArray(out var array, ignoreOrderings: true) - && TranslateExpression(item, applyDefaultTypeMapping: false) is SqlExpression translatedItem: - { - (translatedItem, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, array); - - // We special-case null constant item and use array_position instead, since it does - // nulls correctly (but doesn't use indexes). - // TODO: Better just translate to ANY and handle in nullability processing? - // TODO: once lambda-based caching is implemented, move this to NpgsqlSqlNullabilityProcessor - // (https://github.com/dotnet/efcore/issues/17598) and do for parameters as well. - if (translatedItem is SqlConstantExpression { Value: null }) - { - return BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.IsNotNull( - _sqlExpressionFactory.Function( - "array_position", - [array, translatedItem], - nullable: true, - argumentsPropagateNullability: FalseArrays[2], - typeof(int)))); - } - - return array switch - { - // For array columns which have a GIN index, we translate to array containment (with @>) which uses that index. - ColumnExpression { Column: IColumn column } - when column.Table.Indexes - .Any(i => - i.Columns.Count > 0 - && i.Columns[0] == column - && i.MappedIndexes.Any(mi => mi.GetMethod()?.Equals("GIN", StringComparison.OrdinalIgnoreCase) == true)) - => BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.Contains( - array, - _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). - SqlConstantExpression or PgNewArrayExpression - => base.TranslateContains(source, item), - - // Similar to ParameterExpression below, but when a bare subquery is present inside ANY(), PostgreSQL just compares - // against each of its resulting rows (just like IN). To "extract" the array result of the scalar subquery, we need - // to add an explicit cast (see #1803). - ScalarSubqueryExpression subqueryExpression - => BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.Any( - translatedItem, - _sqlExpressionFactory.Convert( - subqueryExpression, subqueryExpression.Type, subqueryExpression.TypeMapping), - PgAnyOperatorType.Equal)), - - // For ParameterExpression, and for all other cases - e.g. array returned from some function - - // translate to e.SomeText = ANY (@p). This is superior to the general solution which will expand - // parameters to constants, since non-PG SQL does not support arrays. - // Note that this will allow indexes on the item to be used. - _ => BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Any(translatedItem, array, PgAnyOperatorType.Equal)) - }; - } - - // Contains over JSON array: array ? 'foo' for text, array @> item for bool/numeric - case var _ when source.TryExtractJsonArray(out var array, out _, out var isElementNullable, ignoreOrderings: true) - && TranslateExpression(item, applyDefaultTypeMapping: false) is SqlExpression translatedItem: - { - (translatedItem, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, array); - - // TODO: Null semantics - need to coalesce NULL on the item side to 'null'::jsonb. - // TODO: For constant/parameter, use JsonValueReaderWriter to produce the JSON value client-side - return BuildSimplifiedShapedQuery( - source, - _sqlExpressionFactory.Contains( - array, - _sqlExpressionFactory.Function( - "to_jsonb", - [translatedItem is SqlConstantExpression { Value: string } - ? _sqlExpressionFactory.Convert(translatedItem, typeof(string), _typeMappingSource.FindMapping(typeof(string))) - : translatedItem], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(string), - _typeMappingSource.FindMapping("jsonb")))); - } - } - - return base.TranslateContains(source, item); - } - - /// - /// 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? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate) - { - // Simplify x.Array.Count() => cardinality(x.Array) instead of SELECT COUNT(*) FROM unnest(x.Array) - if (predicate is null) - { - switch (source) - { - case var _ when source.TryExtractArray(out var array, ignoreOrderings: true): - { - var translation = _sqlExpressionFactory.Function( - "cardinality", - [array], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)); - -#pragma warning disable EF1001 // SelectExpression constructors are currently internal - return source.Update( - new SelectExpression(translation, _queryCompilationContext.SqlAliasManager), - Expression.Convert( - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)), - typeof(int))); -#pragma warning restore EF1001 - } - - case var _ when source.TryExtractJsonArray(out var jsonArray, out _, out _, ignoreOrderings: true): - { - var translation = _sqlExpressionFactory.Function( - jsonArray.TypeMapping!.StoreType switch - { - "jsonb" => "jsonb_array_length", - "json" => "json_array_length", - _ => throw new UnreachableException() - }, - [jsonArray], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)); - -#pragma warning disable EF1001 // SelectExpression constructors are currently internal - return source.Update( - 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); - } - - /// - /// 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 TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) - { - // 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.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( - projectedColumn1.TypeMapping is not null || projectedColumn2.TypeMapping is not null, - "Concat with no type mapping on either side (operation should be client-evaluated over parameters/constants"); - - // TODO: Conflicting type mappings from both sides? - var inferredTypeMapping = projectedColumn1.TypeMapping ?? projectedColumn2.TypeMapping; - -#pragma warning disable EF1001 // SelectExpression constructors are currently internal - var tableAlias = ((SelectExpression)source1.QueryExpression).Tables.Single().Alias!; - var selectExpression = new SelectExpression( - [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 - Expression shaperExpression = new ProjectionBindingExpression( - selectExpression, new ProjectionMember(), source1.ShaperExpression.Type.MakeNullable()); - - if (source1.ShaperExpression.Type != shaperExpression.Type) - { - Check.DebugAssert( - source1.ShaperExpression.Type.MakeNullable() == shaperExpression.Type, - "expression.Type must be nullable of targetType"); - - shaperExpression = Expression.Convert(shaperExpression, source1.ShaperExpression.Type); - } - - return new ShapedQueryExpression(selectExpression, shaperExpression); - } - - return base.TranslateConcat(source1, source2); - } - - /// - /// 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? TranslateElementAtOrDefault( - ShapedQueryExpression source, - Expression index, - bool returnDefault) - { - if (!returnDefault && TranslateExpression(index) is { } translatedIndex) - { -#pragma warning disable EF1001 // SelectExpression constructors are pubternal - switch (source) - { - // 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. - case var _ when source.TryExtractArray(out var array, out var projectedColumn): - { - return source.UpdateQueryExpression( - new SelectExpression( - _sqlExpressionFactory.ArrayIndex( - array, - // Note that PostgreSQL arrays are 1-based, so adjust the index. - GenerateOneBasedIndexExpression(translatedIndex), projectedColumn.IsNullable), - _queryCompilationContext.SqlAliasManager)); - } - - case var _ when source.TryExtractJsonArray(out var jsonArray, out var projectedElement, out var isElementNullable): - { - var (json, path) = jsonArray is JsonScalarExpression innerJsonScalarExpression - ? (innerJsonScalarExpression.Json, innerJsonScalarExpression.Path.Append(new(translatedIndex)).ToArray()) - : (jsonArray, [new(translatedIndex)]); - - var translation = new JsonScalarExpression( - json, - path, - projectedElement.Type, - projectedElement.TypeMapping, - isElementNullable); - - return source.UpdateQueryExpression(new SelectExpression(translation, _queryCompilationContext.SqlAliasManager)); - } - } -#pragma warning restore EF1001 // SelectExpression constructors are pubternal - } - - return base.TranslateElementAtOrDefault(source, index, returnDefault); - } - - /// - /// 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? TranslateFirstOrDefault( - ShapedQueryExpression source, - LambdaExpression? predicate, - Type returnType, - bool returnDefault) - { - // 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.TryExtractArray(out var array, ignorePredicate: true) - || source.TryConvertToArray(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) - { - translatedPredicate = TranslateLambdaExpression(source, predicate!); - if (translatedPredicate is null) - { - return null; - } - } - - switch (translatedPredicate) - { - // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.IsAncestorOf(e.SomeLTree)) - // Translation: ARRAY['t1','t2'] ?@> e.SomeLTree - // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.IsDescendant(e.SomeLTree)) - // Translation: ARRAY['t1','t2'] ?<@ e.SomeLTree - case PgBinaryExpression - { - OperatorType: (PgExpressionType.Contains or PgExpressionType.ContainedBy) and var operatorType, - Left: ColumnExpression ltreeColumn, - // Contains/ContainedBy can happen for non-LTree types too, so check that - Right: { TypeMapping: NpgsqlLTreeTypeMapping } ltree - } - when ltreeColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - new PgBinaryExpression( - operatorType == PgExpressionType.Contains - ? PgExpressionType.LTreeFirstAncestor - : PgExpressionType.LTreeFirstDescendent, - _sqlExpressionFactory.ApplyDefaultTypeMapping(array), - ltree, - typeof(LTree), - _typeMappingSource.FindMapping(typeof(LTree)))); - } - - // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.MatchesLQuery(lquery)) - // Translation: ARRAY['t1','t2'] ?~ e.lquery - // Pattern match: new[] { "t1", "t2" }.FirstOrDefault(t => t.MatchesLQuery(ltxtquery)) - // Translation: ARRAY['t1','t2'] ?@ e.ltxtquery - case PgBinaryExpression - { - OperatorType: PgExpressionType.LTreeMatches, - Left: ColumnExpression ltreeColumn, - Right: var lquery - } - when ltreeColumn.TableAlias == tableAlias: - { - return BuildSimplifiedShapedQuery( - source, - new PgBinaryExpression( - PgExpressionType.LTreeFirstMatches, - _sqlExpressionFactory.ApplyDefaultTypeMapping(array), - lquery, - typeof(LTree), - _typeMappingSource.FindMapping(typeof(LTree)))); - } - } - } - - return base.TranslateFirstOrDefault(source, predicate, returnType, returnDefault); - } - - /// - /// 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? TranslateSkip(ShapedQueryExpression source, Expression count) - { - // 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.TryExtractArray(out var array, out var projectedColumn) - && TranslateExpression(count) is { } translatedCount) - { -#pragma warning disable EF1001 // SelectExpression constructors are currently internal - var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; - var selectExpression = new SelectExpression( - [ - 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( - 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.TranslateSkip(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? TranslateTake(ShapedQueryExpression source, Expression count) - { - // 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.TryExtractArray(out var array, out var projectedColumn) - && TranslateExpression(count) is { } translatedCount) - { - PgArraySliceExpression sliceExpression; - - // If Skip has been called before, an array slice expression is already there; try to integrate this Take into it. - // Note that we need to take the Skip (lower bound) into account for the Take (upper bound), since the slice upper bound - // operates on the original array (Skip hasn't yet taken place). - if (array is PgArraySliceExpression existingSliceExpression) - { - if (existingSliceExpression is - { - LowerBound: SqlConstantExpression { Value: int lowerBoundValue } lowerBound, - UpperBound: null - }) - { - sliceExpression = existingSliceExpression.Update( - existingSliceExpression.Array, - existingSliceExpression.LowerBound, - translatedCount is SqlConstantExpression { Value: int takeCount } - ? _sqlExpressionFactory.Constant(lowerBoundValue + takeCount - 1, lowerBound.TypeMapping) - : _sqlExpressionFactory.Subtract( - _sqlExpressionFactory.Add(lowerBound, translatedCount), - _sqlExpressionFactory.Constant(1, lowerBound.TypeMapping))); - } - else - { - // For any other case, we allow relational to translate with normal querying. For non-constant lower bounds, we could - // duplicate them into the upper bound, but that could cause expensive double evaluation. - return base.TranslateTake(source, count); - } - } - else - { - sliceExpression = _sqlExpressionFactory.ArraySlice( - array, - lowerBound: null, - upperBound: translatedCount, - projectedColumn.IsNullable); - } - -#pragma warning disable EF1001 // SelectExpression constructors are currently internal - var tableAlias = ((SelectExpression)source.QueryExpression).Tables[0].Alias!; - var selectExpression = new SelectExpression( - [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 - 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.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); - - #region ExecuteUpdate - - /// - /// 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 IsValidSelectExpressionForExecuteUpdate( - SelectExpression selectExpression, - TableExpressionBase targetTable, - [NotNullWhen(true)] out TableExpression? tableExpression) - { - if (!base.IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out tableExpression)) - { - return false; - } - - // PostgreSQL doesn't support referencing the main update table from anywhere except for the UPDATE WHERE clause. - // This specifically makes it impossible to have joins which reference the main table in their predicate (ON ...). - // Because of this, we detect all such inner joins and lift their predicates to the main WHERE clause (where a reference to the - // main table is allowed) - see NpgsqlQuerySqlGenerator.VisitUpdate. - // For any other type of join which contains a reference to the main table, we return false to trigger a subquery pushdown instead. - OuterReferenceFindingExpressionVisitor? visitor = null; - - for (var i = 0; i < selectExpression.Tables.Count; i++) - { - var table = selectExpression.Tables[i]; - - if (ReferenceEquals(table, tableExpression)) - { - continue; - } - - visitor ??= new OuterReferenceFindingExpressionVisitor(tableExpression); - - // For inner joins, if the predicate contains a reference to the main table, NpgsqlQuerySqlGenerator will lift the predicate - // to the WHERE clause; so we only need to check the inner join's table (i.e. subquery) for such a reference. - // Cross join and cross/outer apply (lateral joins) don't have predicates, so just check the entire join for a reference to - // the main table, and switch to subquery syntax if one is found. - // Left join does have a predicate, but it isn't possible to lift it to the main WHERE clause; so also check the entire - // join. - if (table is InnerJoinExpression innerJoin) - { - table = innerJoin.Table; - } - - if (visitor.ContainsReferenceToMainTable(table)) - { - return false; - } - } - - return true; - } - -#pragma warning disable EF9002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - /// - /// 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 TrySerializeScalarToJson( - JsonScalarExpression target, - SqlExpression value, - [NotNullWhen(true)] out SqlExpression? jsonValue) - { - var jsonTypeMapping = ((ColumnExpression)target.Json).TypeMapping!; - - if ( - // The base implementation doesn't handle serializing arbitrary SQL expressions to JSON, since that's - // database-specific. In PostgreSQL we simply do this by wrapping any expression in to_jsonb(). - !base.TrySerializeScalarToJson(target, value, out jsonValue) - // In addition, for string, numeric and bool, the base implementation simply returns the value as-is, since most databases allow - // passing these native types directly to their JSON partial update function. In PostgreSQL, jsonb_set() always requires jsonb, - // so we wrap those expression with to_jsonb() as well. - || jsonValue.TypeMapping?.StoreType is not "jsonb" and not "json") - { - switch (value.TypeMapping!.StoreType) - { - case "jsonb" or "json": - jsonValue = value; - return true; - - case "bytea": - value = _sqlExpressionFactory.Function( - "encode", - [value, _sqlExpressionFactory.Constant("base64")], - nullable: true, - argumentsPropagateNullability: [true, true], - typeof(string), - _typeMappingSource.FindMapping(typeof(string))! - ); - break; - } - - // We now have a scalar value expression that needs to be passed to jsonb_set(), but jsonb_set() requires a json/jsonb - // argument, not e.g. text or int. So we need to wrap the argument in to_jsonb/to_json. - // Note that for structural types we always already get a jsonb/json value and have already exited above (no need for - // to_jsonb/to_json). - - // One exception is if the value expression happens to be a JsonScalarExpression (e.g. copy scalar property from within - // one JSON document into another). For that case, rather than do to_jsonb(x.JsonbDoc ->> 'SomeProperty') - which extracts - // a jsonb property as text only to reconvert it back to jsonb - we just change the type mapping on the JsonScalarExpression - // to json/jsonb, in order to generate x.JsonbDoc -> 'SomeProperty' (no text extraction). - if (value is JsonScalarExpression jsonScalarValue - && jsonScalarValue.Json.TypeMapping?.StoreType == jsonTypeMapping.StoreType) - { - jsonValue = new JsonScalarExpression( - jsonScalarValue.Json, - jsonScalarValue.Path, - jsonScalarValue.Type, - jsonTypeMapping, - jsonScalarValue.IsNullable); - return true; - } - - jsonValue = _sqlExpressionFactory.Function( - jsonTypeMapping.StoreType switch - { - "jsonb" => "to_jsonb", - "json" => "to_json", - _ => throw new UnreachableException() - }, - // Make sure PG interprets constant values correctly by adding explicit typing based on the target property's type mapping. - // Note that we can only be here for scalar properties, for structural types we always already get a jsonb/json value - // and don't need to add to_jsonb/to_json. - [value is SqlConstantExpression ? _sqlExpressionFactory.Convert(value, target.Type, target.TypeMapping) : value], - nullable: true, - argumentsPropagateNullability: [true], - typeof(string), - jsonTypeMapping); - } - - return true; - } -#pragma warning restore EF9002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - - /// - /// 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 SqlExpression? GenerateJsonPartialUpdateSetter( - Expression target, - SqlExpression value, - ref SqlExpression? existingSetterValue) - { - var (jsonColumn, path) = target switch - { - JsonScalarExpression j => ((ColumnExpression)j.Json, j.Path), - JsonQueryExpression j => (j.JsonColumn, j.Path), - - _ => throw new UnreachableException(), - }; - - var jsonSet = _sqlExpressionFactory.Function( - jsonColumn.TypeMapping?.StoreType switch - { - "jsonb" => "jsonb_set", - "json" => "json_set", - _ => throw new UnreachableException() - }, - arguments: - [ - existingSetterValue ?? jsonColumn, - // Hack: Rendering of JSONPATH strings happens in value generation. We can have a special expression for modify to hold the - // IReadOnlyList (just like Json{Scalar,Query}Expression), but instead we do the slight hack of packaging it - // as a constant argument; it will be unpacked and handled in SQL generation. - _sqlExpressionFactory.Constant(path, RelationalTypeMapping.NullMapping), - value - ], - nullable: true, - argumentsPropagateNullability: [true, true, true], - typeof(string), - jsonColumn.TypeMapping); - - if (existingSetterValue is null) - { - return jsonSet; - } - else - { - existingSetterValue = jsonSet; - return null; - } - } - - #endregion ExecuteUpdate - - /// - /// 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 IsValidSelectExpressionForExecuteDelete(SelectExpression selectExpression) - // The default relational behavior is to allow only single-table expressions, and the only permitted feature is a predicate. - // Here we extend this to also inner joins to tables, which we generate via the PostgreSQL-specific USING construct. - => selectExpression is - { - Orderings: [], - Offset: null, - Limit: null, - GroupBy: [], - Having: null - } - && selectExpression.Tables[0] is TableExpression && selectExpression.Tables.Skip(1).All(t => t is InnerJoinExpression); - - // PostgreSQL unnest is guaranteed to return output rows in the same order as its input array, - // https://www.postgresql.org/docs/current/functions-array.html. - /// - protected override bool IsOrdered(SelectExpression selectExpression) - => base.IsOrdered(selectExpression) - || selectExpression.Tables is - [PgTableValuedFunctionExpression { Name: "unnest" or "jsonb_to_recordset" or "json_to_recordset" }]; - - private (ColumnExpression, ValueComparer) GenerateOrdinalityIdentifier(string tableAlias) - { - _ordinalityTypeMapping ??= _typeMappingSource.FindMapping("int")!; - return (new ColumnExpression("ordinality", tableAlias, typeof(int), _ordinalityTypeMapping, nullable: false), - _ordinalityTypeMapping.Comparer); - } - - /// - /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, just increment it. Otherwise, append a +1 in the - /// SQL. - /// - private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) - => expression is SqlConstantExpression constant - ? _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( - new SelectExpression(translation, _queryCompilationContext.SqlAliasManager), - Expression.Convert( - new ProjectionBindingExpression(translation, new ProjectionMember(), typeof(bool?)), typeof(bool))); -#pragma warning restore EF1001 - - private sealed class OuterReferenceFindingExpressionVisitor(TableExpression mainTable) : ExpressionVisitor - { - private bool _containsReference; - - public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) - { - _containsReference = false; - - Visit(tableExpression); - - return _containsReference; - } - - [return: NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - { - if (_containsReference) - { - return expression; - } - - if (expression is ColumnExpression { TableAlias: var tableAlias} && tableAlias == mainTable.Alias) - { - _containsReference = true; - - return expression; - } - - return base.Visit(expression); - } - } -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs deleted file mode 100644 index 39d8377f72..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs +++ /dev/null @@ -1,59 +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 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 - /// 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 NpgsqlQueryableMethodTranslatingExpressionVisitorFactory( - QueryableMethodTranslatingExpressionVisitorDependencies dependencies, - RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, - INpgsqlSingletonOptions npgsqlSingletonOptions) - { - Dependencies = dependencies; - RelationalDependencies = relationalDependencies; - _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 virtual QueryableMethodTranslatingExpressionVisitorDependencies Dependencies { 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. - /// - protected virtual RelationalQueryableMethodTranslatingExpressionVisitorDependencies 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 QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) - => new NpgsqlQueryableMethodTranslatingExpressionVisitor( - Dependencies, - RelationalDependencies, - (RelationalQueryCompilationContext)queryCompilationContext, - _npgsqlSingletonOptions); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypingInjector.cs b/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypingInjector.cs deleted file mode 100644 index 6086bf436e..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlSetOperationTypingInjector.cs +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 736c47e8b1..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlNullabilityProcessor.cs +++ /dev/null @@ -1,741 +0,0 @@ -using System.Runtime.CompilerServices; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; - -/// -public class NpgsqlSqlNullabilityProcessor : SqlNullabilityProcessor -{ - 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 NpgsqlSqlNullabilityProcessor( - RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters) - : base(dependencies, parameters) - { - _sqlExpressionFactory = dependencies.SqlExpressionFactory; - } - - /// - protected override SqlExpression VisitSqlBinary( - SqlBinaryExpression sqlBinaryExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - 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) - { - // 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; - - 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); - } - - visitedLeftValues?.Add(visitedLeftValue); - - if (visitedRightValue != rightValue && visitedRightValues is null) - { - visitedRightValues = SliceToList(rightRowValue.Values, count, i); - } - - visitedRightValues?.Add(visitedRightValue); - - 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, typeMapping: null, existingExpression: sqlBinaryExpression)!, - allowOptimizedExpansion, - out _); - - 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; - } - else - { - expandedExpression = operatorType switch - { - ExpressionType.Equal => _sqlExpressionFactory.AndAlso(expandedExpression, valueBinaryExpression), - ExpressionType.NotEqual => _sqlExpressionFactory.OrElse(expandedExpression, valueBinaryExpression), - _ => throw new UnreachableException() - }; - } - } - - // TODO: This is wrong, need to properly calculate this: #3250 - 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 - : _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)!; - } - - 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) - { - return expandedExpression; - } - - // 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, - new PgRowValueExpression(visitedLeftValues, leftRowValue.Type, leftRowValue.TypeMapping), - new PgRowValueExpression(visitedRightValues, rightRowValue.Type, rightRowValue.TypeMapping), - typeMapping: null)!; - - // Technically the CLR type and type mappings are incorrect, as we're truncating the row values. - // But that shouldn't matter. - 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); - - for (var i = 0; i < count; i++) - { - list.Add(source[i]); - } - - return list; - } - } - } - - /// - protected override SqlExpression VisitCustomSqlExpression( - SqlExpression sqlExpression, - bool allowOptimizedExpansion, - out bool nullable) - => sqlExpression switch - { - PgAnyExpression e => VisitAny(e, allowOptimizedExpansion, out nullable), - PgAllExpression e => VisitAll(e, allowOptimizedExpansion, out nullable), - PgArrayIndexExpression e => VisitArrayIndex(e, allowOptimizedExpansion, out nullable), - PgArraySliceExpression e => VisitArraySlice(e, allowOptimizedExpansion, out nullable), - PgBinaryExpression e => VisitPostgresBinary(e, allowOptimizedExpansion, out nullable), - PgILikeExpression e => VisitILike(e, allowOptimizedExpansion, out nullable), - PgJsonTraversalExpression e => VisitJsonTraversal(e, allowOptimizedExpansion, out nullable), - PgNewArrayExpression e => VisitNewArray(e, allowOptimizedExpansion, out nullable), - PgRegexMatchExpression e => VisitRegexMatch(e, allowOptimizedExpansion, out nullable), - PgRowValueExpression e => VisitRowValueExpression(e, allowOptimizedExpansion, out nullable), - PgUnknownBinaryExpression e => VisitUnknownBinary(e, allowOptimizedExpansion, out nullable), - - // PostgresFunctionExpression is visited via the SqlFunctionExpression override below - - _ => base.VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) - }; - - /// - /// 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 SqlExpression VisitAny(PgAnyExpression anyExpression, bool allowOptimizedExpansion, out bool nullable) - { - Check.NotNull(anyExpression, nameof(anyExpression)); - - var item = Visit(anyExpression.Item, out var itemNullable); - var array = Visit(anyExpression.Array, out var entireArrayNullable); - - SqlExpression updated = anyExpression.Update(item, array); - - if (UseRelationalNulls) - { - nullable = false; - return updated; - } - - // We only do null compensation for item = ANY(array), not for any other operator. - // Note that LIKE (without ANY) doesn't get null-compensated either, so we're consistent with that. - if (anyExpression.OperatorType != PgAnyOperatorType.Equal) - { - // If the array is a constant and it contains a null, or if the array isn't a constant, - // we assume the entire expression can be null. - nullable = itemNullable || entireArrayNullable || MayContainNulls(anyExpression.Array); - return updated; - } - - // For PostgresAnyOperatorType.Equal, we perform null compensation. - // When the array is a parameter, we don't look at the values (in contrast to relational's - // VisitIn which expands parameter arrays into constants). - - // Note that ANY does not use GIN/GIST indexes on the array, but it does use indexes on the item. - // All of the below expansions allow index use on the item. - - // non_nullable = ANY(array) -> non_nullable = ANY(array) (optimized) - // non_nullable = ANY(array) -> non_nullable = ANY(array) AND (non_nullable = ANY(array) IS NOT NULL) (full) - // nullable = ANY(array) -> nullable = ANY(array) OR (nullable IS NULL AND array_position(array, NULL) IS NOT NULL) (optimized) - // nullable = ANY(array) -> (nullable = ANY(array) AND (nullable = ANY(array) IS NOT NULL)) OR (nullable IS NULL AND array_position(array, NULL) IS NOT NULL) (full) - - nullable = false; - - // ANY returns NULL if an element isn't found in the array but the array contains NULL, instead of false. - // So for non-optimized, we compensate by adding a check that NULL isn't returned. - if (!allowOptimizedExpansion) - { - updated = _sqlExpressionFactory.And(updated, _sqlExpressionFactory.IsNotNull(updated)); - } - - if (!itemNullable) - { - return updated; - } - - // If the item is nullable, add an OR to check for the item being null and the array containing null. - // The latter check is done with array_position, which returns null when a value was not found, and - // a position if the item (including null!) was found (IS NOT DISTINCT FROM semantics) - return _sqlExpressionFactory.OrElse( - updated, - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNull(item), - _sqlExpressionFactory.IsNotNull( - _sqlExpressionFactory.Function( - "array_position", - [array, _sqlExpressionFactory.Constant(null, item.Type, item.TypeMapping)], - nullable: true, - argumentsPropagateNullability: FalseArrays[2], - 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. - /// - protected virtual SqlExpression VisitAll(PgAllExpression allExpression, bool allowOptimizedExpansion, out bool nullable) - { - Check.NotNull(allExpression, nameof(allExpression)); - - var item = Visit(allExpression.Item, out var itemNullable); - var array = Visit(allExpression.Array, out var entireArrayNullable); - - SqlExpression updated = allExpression.Update(item, array); - - if (UseRelationalNulls) - { - nullable = false; - return updated; - } - - // If the array is a constant and it contains a null, or if the array isn't a constant, - // we assume the entire expression can be null. - nullable = itemNullable || entireArrayNullable || MayContainNulls(allExpression.Array); - return updated; - } - - /// - /// 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 SqlExpression VisitArrayIndex( - PgArrayIndexExpression arrayIndexExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(arrayIndexExpression, nameof(arrayIndexExpression)); - - var array = Visit(arrayIndexExpression.Array, allowOptimizedExpansion, out var arrayNullable); - var index = Visit(arrayIndexExpression.Index, allowOptimizedExpansion, out var indexNullable); - - nullable = arrayNullable || indexNullable || arrayIndexExpression.IsNullable; - - return arrayIndexExpression.Update(array, index); - } - - /// - /// 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 SqlExpression VisitArraySlice( - PgArraySliceExpression arraySliceExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(arraySliceExpression, nameof(arraySliceExpression)); - - var array = Visit(arraySliceExpression.Array, allowOptimizedExpansion, out var arrayNullable); - var lowerBound = Visit(arraySliceExpression.LowerBound, allowOptimizedExpansion, out var lowerBoundNullable); - var upperBound = Visit(arraySliceExpression.UpperBound, allowOptimizedExpansion, out var upperBoundNullable); - - nullable = arrayNullable || lowerBoundNullable || upperBoundNullable || arraySliceExpression.IsNullable; - - return arraySliceExpression.Update(array, lowerBound, upperBound); - } - - /// - /// 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 SqlExpression VisitPostgresBinary( - PgBinaryExpression binaryExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(binaryExpression, nameof(binaryExpression)); - - // For arrays Contains (arr1 @> arr2), we do not implement null semantics because doing so would prevent - // index use. So '{1, 2, NULL}' @> '{NULL}' returns false. This is notably the translation for .NET - // Contains over column arrays. - - var left = Visit(binaryExpression.Left, allowOptimizedExpansion, out var leftNullable); - var right = Visit(binaryExpression.Right, allowOptimizedExpansion, out var rightNullable); - - nullable = binaryExpression.OperatorType switch - { - // The following LTree search methods return null for "not found" - PgExpressionType.LTreeFirstAncestor => true, - PgExpressionType.LTreeFirstDescendent => true, - PgExpressionType.LTreeFirstMatches => true, - - _ => leftNullable || rightNullable - }; - - return binaryExpression.Update(left, right); - } - - /// - /// 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 SqlExpression VisitSqlFunction( - SqlFunctionExpression sqlFunctionExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - // PostgresFunctionExpression extends SqlFunctionExpression, and adds aggregate predicate and ordering expressions to that. - // First call the base VisitSqlFunction to visit the arguments - var visitedBase = base.VisitSqlFunction(sqlFunctionExpression, allowOptimizedExpansion, out nullable); - - // base.VisitSqlFunction has some special logic for SUM which wraps it in a COALESCE - // (see https://github.com/dotnet/efcore/issues/28158), so we need some special handling to properly visit the - // PostgresFunctionExpression it wraps. - if (sqlFunctionExpression.IsBuiltIn - && string.Equals(sqlFunctionExpression.Name, "SUM", StringComparison.OrdinalIgnoreCase) - && visitedBase is SqlFunctionExpression { Name: "COALESCE", Arguments: { } } coalesceExpression - && coalesceExpression.Arguments[0] is PgFunctionExpression wrappedFunctionExpression) - { - // The base logic assumes sum is operating over numbers, which breaks sum over PG interval. - // Detect that case and remove the coalesce entirely (note that we don't need coalescing since sum function is in - // EF.Functions.Sum, and returns nullable. This is a temporary hack until #38158 is fixed. - if (sqlFunctionExpression.Type == typeof(TimeSpan) - || sqlFunctionExpression.Type.FullName is "NodaTime.Period" or "NodaTime.Duration") - { - return coalesceExpression.Arguments[0]; - } - - var visitedArguments = coalesceExpression.Arguments!.ToArray(); - visitedArguments[0] = VisitPostgresFunctionComponents(wrappedFunctionExpression); - - return coalesceExpression.Update(coalesceExpression.Instance, visitedArguments); - } - - return visitedBase is PgFunctionExpression pgFunctionExpression - ? VisitPostgresFunctionComponents(pgFunctionExpression) - : visitedBase; - - PgFunctionExpression VisitPostgresFunctionComponents(PgFunctionExpression pgFunctionExpression) - { - var aggregateChanged = false; - - var visitedAggregatePredicate = Visit(pgFunctionExpression.AggregatePredicate, allowOptimizedExpansion: true, out _); - aggregateChanged |= visitedAggregatePredicate != pgFunctionExpression.AggregatePredicate; - - OrderingExpression[]? visitedOrderings = null; - for (var i = 0; i < pgFunctionExpression.AggregateOrderings.Count; i++) - { - var ordering = pgFunctionExpression.AggregateOrderings[i]; - var visitedOrdering = ordering.Update(Visit(ordering.Expression, out _)); - if (visitedOrdering != ordering && visitedOrderings is null) - { - visitedOrderings = new OrderingExpression[pgFunctionExpression.AggregateOrderings.Count]; - for (var j = 0; j < i; j++) - { - visitedOrderings[j] = pgFunctionExpression.AggregateOrderings[j]; - } - - aggregateChanged = true; - } - - if (visitedOrderings is not null) - { - visitedOrderings[i] = visitedOrdering; - } - } - - return aggregateChanged - ? pgFunctionExpression.UpdateAggregateComponents( - visitedAggregatePredicate, - visitedOrderings ?? pgFunctionExpression.AggregateOrderings) - : pgFunctionExpression; - } - } - - /// - /// 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 SqlExpression VisitILike(PgILikeExpression iLikeExpression, bool allowOptimizedExpansion, out bool nullable) - { - // 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; - - return result; - } - - nullable = false; - - // 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; - } - - /// - /// 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 SqlExpression VisitNewArray( - PgNewArrayExpression newArrayExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(newArrayExpression, nameof(newArrayExpression)); - - SqlExpression[]? newInitializers = null; - for (var i = 0; i < newArrayExpression.Expressions.Count; i++) - { - var initializer = newArrayExpression.Expressions[i]; - var newInitializer = Visit(initializer, allowOptimizedExpansion, out _); - if (newInitializer != initializer && newInitializers is null) - { - newInitializers = new SqlExpression[newArrayExpression.Expressions.Count]; - for (var j = 0; j < i; j++) - { - newInitializers[j] = newArrayExpression.Expressions[j]; - } - } - - if (newInitializers is not null) - { - newInitializers[i] = newInitializer; - } - } - - nullable = false; - return newInitializers is null - ? newArrayExpression - : new PgNewArrayExpression(newInitializers, newArrayExpression.Type, newArrayExpression.TypeMapping); - } - - /// - /// 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 SqlExpression VisitRegexMatch( - PgRegexMatchExpression regexMatchExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(regexMatchExpression, nameof(regexMatchExpression)); - - var match = Visit(regexMatchExpression.Match, out var matchNullable); - var pattern = Visit(regexMatchExpression.Pattern, out var patternNullable); - - nullable = matchNullable || patternNullable; - - return regexMatchExpression.Update(match, pattern); - } - - /// - /// 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 SqlExpression VisitJsonTraversal( - PgJsonTraversalExpression jsonTraversalExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(jsonTraversalExpression, nameof(jsonTraversalExpression)); - - var expression = Visit(jsonTraversalExpression.Expression, out _); - - SqlExpression[]? newPath = null; - for (var i = 0; i < jsonTraversalExpression.Path.Count; i++) - { - var pathComponent = jsonTraversalExpression.Path[i]; - var newPathComponent = Visit(pathComponent, allowOptimizedExpansion, out _); - if (newPathComponent != pathComponent && newPath is null) - { - newPath = new SqlExpression[jsonTraversalExpression.Path.Count]; - for (var j = 0; j < i; j++) - { - newPath[j] = jsonTraversalExpression.Path[j]; - } - } - - if (newPath is not null) - { - newPath[i] = newPathComponent; - } - } - - // For now, anything inside a JSON document is considered nullable. - // See #1851 for optimizing this for JSON POCO mapping. - nullable = true; - - return jsonTraversalExpression.Update(expression, newPath?.ToArray() ?? jsonTraversalExpression.Path); - } - - /// - /// 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 SqlExpression VisitRowValueExpression( - PgRowValueExpression rowValueExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - SqlExpression[]? newValues = null; - - for (var i = 0; i < rowValueExpression.Values.Count; i++) - { - var value = rowValueExpression.Values[i]; - - // Note that we disallow optimized expansion, since the null vs. false distinction does matter inside the row's values - var newValue = Visit(value, allowOptimizedExpansion: false, out _); - if (newValue != value && newValues is null) - { - newValues = new SqlExpression[rowValueExpression.Values.Count]; - for (var j = 0; j < i; j++) - { - newValues[j] = rowValueExpression.Values[j]; - } - } - - if (newValues is not null) - { - newValues[i] = newValue; - } - } - - // The row value expression itself can never be null - nullable = false; - - return rowValueExpression.Update(newValues ?? rowValueExpression.Values); - } - - /// - /// Visits a and computes its nullability. - /// - /// - /// - /// A expression to visit. - /// A bool value indicating if optimized expansion which considers null value as false value is allowed. - /// A bool value indicating whether the sql expression is nullable. - /// An optimized sql expression. - protected virtual SqlExpression VisitUnknownBinary( - PgUnknownBinaryExpression unknownBinaryExpression, - bool allowOptimizedExpansion, - out bool nullable) - { - Check.NotNull(unknownBinaryExpression, nameof(unknownBinaryExpression)); - - var left = Visit(unknownBinaryExpression.Left, allowOptimizedExpansion, out var leftNullable); - var right = Visit(unknownBinaryExpression.Right, allowOptimizedExpansion, out var rightNullable); - - nullable = leftNullable || rightNullable; - - return unknownBinaryExpression.Update(left, right); - } - - private static bool MayContainNulls(SqlExpression arrayExpression) - { - if (arrayExpression is SqlConstantExpression { Value: Array constantArray }) - { - for (var i = 0; i < constantArray.Length; i++) - { - if (constantArray.GetValue(i) is null) - { - return true; - } - } - - return false; - } - - 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 } && ParametersDecorator.IsNull(parameterName); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs deleted file mode 100644 index 7211b966c4..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ /dev/null @@ -1,976 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Net; -using System.Runtime.CompilerServices; -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.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.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 NpgsqlSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor -{ - private readonly QueryCompilationContext _queryCompilationContext; - private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; - private readonly IRelationalTypeMappingSource _typeMappingSource; - private readonly NpgsqlJsonPocoTranslator _jsonPocoTranslator; - - private readonly RelationalTypeMapping _timestampMapping; - private readonly RelationalTypeMapping _timestampTzMapping; - - private static Type? _nodaTimePeriodType; - - private static readonly ConstructorInfo DateTimeCtor1 = - typeof(DateTime).GetConstructor([typeof(int), typeof(int), typeof(int)])!; - - private static readonly ConstructorInfo DateTimeCtor2 = - typeof(DateTime).GetConstructor([typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int)])!; - - private static readonly ConstructorInfo DateTimeCtor3 = - typeof(DateTime).GetConstructor( - [typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(DateTimeKind)])!; - - private static readonly ConstructorInfo DateOnlyCtor = - typeof(DateOnly).GetConstructor([typeof(int), typeof(int), typeof(int)])!; - - private static readonly MethodInfo StringStartsWithMethod - = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), [typeof(string)])!; - - private static readonly MethodInfo StringEndsWithMethod - = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), [typeof(string)])!; - - private static readonly MethodInfo StringContainsMethod - = typeof(string).GetRuntimeMethod(nameof(string.Contains), [typeof(string)])!; - - private static readonly MethodInfo EscapeLikePatternParameterMethod = - typeof(NpgsqlSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!; - - // Note: This is the PostgreSQL default and does not need to be explicitly specified - private const char LikeEscapeChar = '\\'; - - /// - /// 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 NpgsqlSqlTranslatingExpressionVisitor( - RelationalSqlTranslatingExpressionVisitorDependencies dependencies, - QueryCompilationContext queryCompilationContext, - QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) - : base(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor) - { - _queryCompilationContext = queryCompilationContext; - _sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory; - _jsonPocoTranslator = ((NpgsqlMemberTranslatorProvider)Dependencies.MemberTranslatorProvider).JsonPocoTranslator; - _typeMappingSource = dependencies.TypeMappingSource; - _timestampMapping = _typeMappingSource.FindMapping("timestamp without time zone")!; - _timestampTzMapping = _typeMappingSource.FindMapping("timestamp with time zone")!; - } - - /// - /// 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 VisitConditional(ConditionalExpression conditionalExpression) - { - var test = Visit(conditionalExpression.Test); - var ifTrue = Visit(conditionalExpression.IfTrue); - var ifFalse = Visit(conditionalExpression.IfFalse); - - if (TranslationFailed(conditionalExpression.Test, test, out var sqlTest) - || TranslationFailed(conditionalExpression.IfTrue, ifTrue, out var sqlIfTrue) - || TranslationFailed(conditionalExpression.IfFalse, ifFalse, out var sqlIfFalse)) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - // Translate: - // a == b ? null : a -> NULLIF(a, b) - // a != b ? a : null -> NULLIF(a, b) - if (sqlTest is SqlBinaryExpression binary && sqlIfTrue is not null && sqlIfFalse is not null) - { - switch (binary.OperatorType) - { - case ExpressionType.Equal - when ifTrue is SqlConstantExpression { Value: null } && TryTranslateToNullIf(sqlIfFalse, out var nullIfTranslation): - case ExpressionType.NotEqual - when ifFalse is SqlConstantExpression { Value: null } && TryTranslateToNullIf(sqlIfTrue, out nullIfTranslation): - return nullIfTranslation; - } - } - - return _sqlExpressionFactory.Case([new CaseWhenClause(sqlTest!, sqlIfTrue!)], sqlIfFalse); - - bool TryTranslateToNullIf(SqlExpression conditionalResult, [NotNullWhen(true)] out Expression? nullIfTranslation) - { - var (left, right) = (binary.Left, binary.Right); - - if (left.Equals(conditionalResult)) - { - nullIfTranslation = _sqlExpressionFactory.Function( - "NULLIF", [left, right], true, [false, false], left.Type, left.TypeMapping); - return true; - } - - if (right.Equals(conditionalResult)) - { - nullIfTranslation = _sqlExpressionFactory.Function( - "NULLIF", [right, left], true, [false, false], right.Type, right.TypeMapping); - return true; - } - - nullIfTranslation = null; - return false; - } - } - - /// - /// 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 VisitUnary(UnaryExpression unaryExpression) - { - switch (unaryExpression.NodeType) - { - case ExpressionType.ArrayLength: - if (TranslationFailed(unaryExpression.Operand, Visit(unaryExpression.Operand), out var sqlOperand)) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - // Translate Length on byte[], but only if the type mapping is for bytea. - // For byte[] mapped to an actual PG array (smallint[]), that's a primitive collection, and ArrayLength gets transformed to - // Count() which gets translated to cardinality() as usual in NpgsqlQueryableMethodTranslatingExpressionVisitor. - if (sqlOperand!.Type == typeof(byte[]) && sqlOperand.TypeMapping is NpgsqlByteArrayTypeMapping or null) - { - return _sqlExpressionFactory.Function( - "length", - [sqlOperand], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - typeof(int)); - } - - // Attempt to translate Length on a JSON POCO array - if (_jsonPocoTranslator.TranslateArrayLength(sqlOperand) is SqlExpression translation) - { - return translation; - } - - // Note that Length over PG arrays (not within JSON) gets translated by QueryableMethodTranslatingEV, since arrays are - // primitive collections - break; - - // We have row value comparison methods such as EF.Functions.GreaterThan, which accept two ValueTuples/Tuples. - // Since they accept ITuple parameters, the arguments have a Convert node casting up from the concrete argument to ITuple; - // this node causes translation failure in RelationalSqlTranslatingExpressionVisitor, so unwrap here. - case ExpressionType.Convert - when unaryExpression.Type == typeof(ITuple) && unaryExpression.Operand.Type.IsAssignableTo(typeof(ITuple)): - return Visit(unaryExpression.Operand); - - // We map both IPAddress and NpgsqlInet to PG inet, and translate many methods accepting NpgsqlInet, so ignore casts from - // IPAddress to NpgsqlInet. - // On the PostgreSQL side, cidr is also implicitly convertible to inet, and at the ADO.NET level NpgsqlCidr has a similar - // implicit conversion operator to NpgsqlInet. So remove that cast as well. - case ExpressionType.Convert - when unaryExpression.Type == typeof(NpgsqlInet) - && (unaryExpression.Operand.Type == typeof(IPAddress) - || unaryExpression.Operand.Type == typeof(IPNetwork) -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - || unaryExpression.Operand.Type == typeof(NpgsqlCidr)): -#pragma warning restore CS0618 - return Visit(unaryExpression.Operand); - } - - return base.VisitUnary(unaryExpression); - } - - /// - /// 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 VisitNewArray(NewArrayExpression newArrayExpression) - { - if (base.VisitNewArray(newArrayExpression) is SqlExpression visitedNewArrayExpression) - { - return visitedNewArrayExpression; - } - - if (newArrayExpression.NodeType == ExpressionType.NewArrayInit) - { - var visitedExpressions = new SqlExpression[newArrayExpression.Expressions.Count]; - for (var i = 0; i < newArrayExpression.Expressions.Count; i++) - { - if (Visit(newArrayExpression.Expressions[i]) is SqlExpression visited) - { - visitedExpressions[i] = visited; - } - else - { - return QueryCompilationContext.NotTranslatedExpression; - } - } - - return _sqlExpressionFactory.NewArray(visitedExpressions, newArrayExpression.Type); - } - - return QueryCompilationContext.NotTranslatedExpression; - } - - /// - /// 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 VisitBinary(BinaryExpression binaryExpression) - { - switch (binaryExpression.NodeType) - { - case ExpressionType.Subtract - when binaryExpression.Left.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate" - && binaryExpression.Right.Type.UnwrapNullableType().FullName == "NodaTime.LocalDate": - { - if (TranslationFailed(binaryExpression.Left, Visit(TryRemoveImplicitConvert(binaryExpression.Left)), out var sqlLeft) - || TranslationFailed(binaryExpression.Right, Visit(TryRemoveImplicitConvert(binaryExpression.Right)), out var sqlRight)) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - var subtraction = _sqlExpressionFactory.MakeBinary( - ExpressionType.Subtract, sqlLeft!, sqlRight!, _typeMappingSource.FindMapping(typeof(int)))!; - - return PgFunctionExpression.CreateWithNamedArguments( - "make_interval", - [subtraction], - ["days"], - nullable: true, - argumentsPropagateNullability: TrueArrays[1], - builtIn: true, - _nodaTimePeriodType ??= binaryExpression.Left.Type.Assembly.GetType("NodaTime.Period")!, - typeMapping: null); - - // Note: many other date/time arithmetic operators are fully supported as-is by PostgreSQL - see NpgsqlSqlExpressionFactory - } - - case ExpressionType.ArrayIndex: - { - // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see NpgsqlArrayTranslator - Check.DebugFail( - "During preprocessing, ArrayIndex and List[] get normalized to ElementAt; see NpgsqlArrayTranslator. " - + "Should never see ArrayIndex."); - break; - } - } - - var translation = base.VisitBinary(binaryExpression); - - 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 SqlBinaryExpression - { - OperatorType: ExpressionType.Equal or ExpressionType.NotEqual, - Left: JsonScalarExpression { TypeMapping: NpgsqlStructuralJsonTypeMapping } operand, - Right: SqlConstantExpression { Value: null } - } binary: - { - return binary.Update( - new JsonScalarExpression( - operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable), - binary.Right); - } - case SqlBinaryExpression - { - OperatorType: ExpressionType.Equal or ExpressionType.NotEqual, - Left: SqlConstantExpression { Value: null }, - Right: JsonScalarExpression { TypeMapping: NpgsqlStructuralJsonTypeMapping } operand - } binary: - { - return binary.Update( - binary.Left, - new JsonScalarExpression( - operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable)); - } - // Unfortunately EF isn't consistent in its representation of X IS NULL in the SQL tree - sometimes it's a SqlUnaryExpression with Equals, - // sometimes it's an X = NULL SqlBinaryExpression that later gets transformed to SqlUnaryExpression, in SqlNullabilityProcessor. We recognize - // both of these here. - case SqlUnaryExpression - { - Operand: JsonScalarExpression { TypeMapping: NpgsqlStructuralJsonTypeMapping } operand - } unary: - return unary.Update( - new JsonScalarExpression( - operand.Json, operand.Path, operand.Type, _typeMappingSource.FindMapping("text"), operand.IsNullable)); - } - - return translation; - } - - /// - /// 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 VisitMethodCall(MethodCallExpression methodCallExpression) - { - var method = methodCallExpression.Method; - - // Pattern-match: cube.LowerLeft[index] or cube.UpperRight[index] - // This appears as: get_Item method call on a MemberExpression of LowerLeft/UpperRight - if (methodCallExpression is - { - Method.Name: "get_Item", - Object: MemberExpression - { - Member.Name: nameof(NpgsqlCube.LowerLeft) or nameof(NpgsqlCube.UpperRight) - } memberExpression - } && memberExpression.Member.DeclaringType == typeof(NpgsqlCube)) - { - // Translate the cube instance and index argument - if (Visit(memberExpression.Expression) is not SqlExpression sqlCubeInstance - || Visit(methodCallExpression.Arguments[0]) is not SqlExpression sqlIndex) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - // Convert zero-based to one-based index - // For constants, optimize at translation time; for parameters/columns, add at runtime - var pgIndex = sqlIndex is SqlConstantExpression { Value: int index } - ? _sqlExpressionFactory.Constant(index + 1) - : _sqlExpressionFactory.Add(sqlIndex, _sqlExpressionFactory.Constant(1)); - - // Determine which function to call - var functionName = memberExpression.Member.Name == nameof(NpgsqlCube.LowerLeft) - ? "cube_ll_coord" - : "cube_ur_coord"; - - return _sqlExpressionFactory.Function( - functionName, - [sqlCubeInstance, pgIndex], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(double)); - } - - // Pattern-match: cube.ToSubset(indexes) - if (method.Name == nameof(NpgsqlCube.ToSubset) - && method.DeclaringType == typeof(NpgsqlCube) - && methodCallExpression.Object is not null) - { - // Translate cube instance and indexes array - if (Visit(methodCallExpression.Object) is not SqlExpression sqlCubeInstance - || Visit(methodCallExpression.Arguments[0]) is not SqlExpression sqlIndexes) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - return TranslateCubeToSubset(sqlCubeInstance, sqlIndexes) ?? QueryCompilationContext.NotTranslatedExpression; - } - - if (method == StringStartsWithMethod - && TryTranslateStartsEndsWithContains( - methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.StartsWith, out var translation1)) - { - return translation1; - } - - if (method == StringEndsWithMethod - && TryTranslateStartsEndsWithContains( - methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.EndsWith, out var translation2)) - { - return translation2; - } - - if (method == StringContainsMethod - && TryTranslateStartsEndsWithContains( - methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.Contains, out var translation3)) - { - return translation3; - } - - return base.VisitMethodCall(methodCallExpression); - } - - private SqlExpression? TranslateCubeToSubset(SqlExpression cubeExpression, SqlExpression indexesExpression) - { - SqlExpression convertedIndexes; - - switch (indexesExpression) - { - // Parameters or columns - create subquery to convert 0-based to 1-based at runtime - case SqlParameterExpression or ColumnExpression: - { - // Apply type mapping to the indexes array - var intArrayTypeMapping = _typeMappingSource.FindMapping(typeof(int[]))!; - var typedIndexes = _sqlExpressionFactory.ApplyTypeMapping(indexesExpression, intArrayTypeMapping); - - // Generate table alias and create unnest table - var tableAlias = ((RelationalQueryCompilationContext)_queryCompilationContext).SqlAliasManager.GenerateTableAlias("u"); - var unnestTable = new PgUnnestExpression(tableAlias, typedIndexes, "x", withOrdinality: false); - - // Create column reference for unnested value - var intTypeMapping = _typeMappingSource.FindMapping(typeof(int))!; - var xColumn = new ColumnExpression("x", tableAlias, typeof(int), intTypeMapping, nullable: false); - - // Create increment expression: x + 1 - var xPlusOne = _sqlExpressionFactory.Add(xColumn, _sqlExpressionFactory.Constant(1, intTypeMapping)); - - // Create array_agg(x + 1) function - var arrayAggFunction = _sqlExpressionFactory.Function( - "array_agg", - [xPlusOne], - nullable: true, - argumentsPropagateNullability: new[] { true }, - typeof(int[]), - intArrayTypeMapping); - - // Construct SelectExpression -#pragma warning disable EF1001 // SelectExpression constructors are pubternal - var selectExpression = new SelectExpression( - [unnestTable], - arrayAggFunction, - [], - ((RelationalQueryCompilationContext)_queryCompilationContext).SqlAliasManager); -#pragma warning restore EF1001 - - // Finalize and wrap in ScalarSubqueryExpression - selectExpression.ApplyProjection(); - convertedIndexes = new ScalarSubqueryExpression(selectExpression); - break; - } - - // Constant arrays - convert directly at compile time - case SqlConstantExpression { Value: int[] constantArray }: - { - var oneBasedValues = constantArray.Select(i => i + 1).ToArray(); - convertedIndexes = _sqlExpressionFactory.Constant(oneBasedValues); - break; - } - - // Inline arrays (new[] { ... }) - convert each element - case PgNewArrayExpression { Expressions: var expressions }: - { - var convertedExpressions = expressions - .Select(e => e is SqlConstantExpression { Value: int index } - ? _sqlExpressionFactory.Constant(index + 1) // Constant element - : _sqlExpressionFactory.Add(e, _sqlExpressionFactory.Constant(1))) // Non-constant element - .ToArray(); - convertedIndexes = _sqlExpressionFactory.NewArray(convertedExpressions, typeof(int[])); - break; - } - - default: - // Unexpected case - cannot translate - return null; - } - - // Build final cube_subset function call - return _sqlExpressionFactory.Function( - "cube_subset", - [cubeExpression, convertedIndexes], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(NpgsqlCube), - _typeMappingSource.FindMapping(typeof(NpgsqlCube))); - } - - /// - /// 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 VisitNew(NewExpression newExpression) - { - var visitedNewExpression = base.VisitNew(newExpression); - - if (visitedNewExpression != QueryCompilationContext.NotTranslatedExpression) - { - return visitedNewExpression; - } - - // We translate new ValueTuple(x, y...) to a SQL row value expression: (x, y). - // This is notably done to support row value comparisons: WHERE (x, y) > (3, 4) (see e.g. NpgsqlDbFunctionsExtensions.GreaterThan) - if (newExpression.Type.IsAssignableTo(typeof(ITuple))) - { - return TryTranslateArguments(out var sqlArguments) - ? new PgRowValueExpression(sqlArguments, newExpression.Type) - : QueryCompilationContext.NotTranslatedExpression; - } - - // Translate new DateTime(...) -> make_timestamp/make_date - if (newExpression.Constructor?.DeclaringType == typeof(DateTime)) - { - if (newExpression.Constructor == DateTimeCtor1) - { - return TryTranslateArguments(out var sqlArguments) - ? _sqlExpressionFactory.Function( - "make_date", sqlArguments, nullable: true, TrueArrays[3], typeof(DateTime), _timestampMapping) - : QueryCompilationContext.NotTranslatedExpression; - } - - if (newExpression.Constructor == DateTimeCtor2) - { - if (!TryTranslateArguments(out var sqlArguments)) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - // DateTime's second component is an int, but PostgreSQL's MAKE_TIMESTAMP accepts a double precision - sqlArguments[5] = _sqlExpressionFactory.Convert(sqlArguments[5], typeof(double)); - - return _sqlExpressionFactory.Function( - "make_timestamp", sqlArguments, nullable: true, TrueArrays[6], typeof(DateTime), _timestampMapping); - } - - if (newExpression.Constructor == DateTimeCtor3 - && newExpression.Arguments[6] is ConstantExpression { Value : DateTimeKind kind }) - { - if (!TryTranslateArguments(out var sqlArguments)) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - // DateTime's second component is an int, but PostgreSQL's make_timestamp/make_timestamptz accepts a double precision. - // Also chop off the last Kind argument which does not get sent to PostgreSQL - var rewrittenArguments = new List - { - sqlArguments[0], - sqlArguments[1], - sqlArguments[2], - sqlArguments[3], - sqlArguments[4], - _sqlExpressionFactory.Convert(sqlArguments[5], typeof(double)) - }; - - if (kind == DateTimeKind.Utc) - { - rewrittenArguments.Add(_sqlExpressionFactory.Constant("UTC")); - } - - return _sqlExpressionFactory.Function( - kind == DateTimeKind.Utc ? "make_timestamptz" : "make_timestamp", - rewrittenArguments, - nullable: true, - TrueArrays[rewrittenArguments.Count], - typeof(DateTime), - kind == DateTimeKind.Utc ? _timestampTzMapping : _timestampMapping); - } - } - - // Translate new DateOnly(...) -> make_date - if (newExpression.Constructor == DateOnlyCtor) - { - return TryTranslateArguments(out var sqlArguments) - ? _sqlExpressionFactory.Function( - "make_date", sqlArguments, nullable: true, TrueArrays[3], typeof(DateOnly)) - : QueryCompilationContext.NotTranslatedExpression; - } - - // Translate new NpgsqlCube(...) -> cube(...) - if (newExpression.Constructor?.DeclaringType == typeof(NpgsqlCube)) - { - if (!TryTranslateArguments(out var sqlArguments)) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - var cubeTypeMapping = _typeMappingSource.FindMapping(typeof(NpgsqlCube)); - var cubeParameters = newExpression.Constructor.GetParameters(); - - // Distinguish constructor overloads by parameter patterns - switch (cubeParameters) - { - case [var pCoords] when pCoords.ParameterType.IsAssignableFrom(typeof(double)) - || typeof(IEnumerable).IsAssignableFrom(pCoords.ParameterType): - // NpgsqlCube(double coord) or NpgsqlCube(IEnumerable coords) - case [var pCoord1, var pCoord2] when pCoord1.ParameterType.IsAssignableFrom(typeof(double)) - && pCoord2.ParameterType.IsAssignableFrom(typeof(double)): - // NpgsqlCube(double coord1, double coord2) - case [var pLowerLeft, var pUpperRight] - when typeof(IEnumerable).IsAssignableFrom(pLowerLeft.ParameterType) - && typeof(IEnumerable).IsAssignableFrom(pUpperRight.ParameterType): - // NpgsqlCube(IEnumerable lowerLeft, IEnumerable upperRight) - case [var pCube, var pCoord] when pCube.ParameterType.IsAssignableFrom(typeof(NpgsqlCube)) - && pCoord.ParameterType.IsAssignableFrom(typeof(double)): - // NpgsqlCube(NpgsqlCube cube, double coord) - case [var pCube2, var pCoord12, var pCoord22] - when pCube2.ParameterType.IsAssignableFrom(typeof(NpgsqlCube)) - && pCoord12.ParameterType.IsAssignableFrom(typeof(double)) - && pCoord22.ParameterType.IsAssignableFrom(typeof(double)): - // NpgsqlCube(NpgsqlCube cube, double coord1, double coord2) - // All cases fallthrough to single cube() expression - // cube() is a STRICT function - returns NULL if any argument is NULL - return _sqlExpressionFactory.Function( - "cube", - sqlArguments, - nullable: true, - argumentsPropagateNullability: TrueArrays[sqlArguments.Length], - typeof(NpgsqlCube), - cubeTypeMapping); - } - } - - return QueryCompilationContext.NotTranslatedExpression; - - bool TryTranslateArguments(out SqlExpression[] sqlArguments) - { - sqlArguments = new SqlExpression[newExpression.Arguments.Count]; - for (var i = 0; i < sqlArguments.Length; i++) - { - var argument = newExpression.Arguments[i]; - if (TranslationFailed(argument, Visit(argument), out var sqlArgument)) - { - return false; - } - - sqlArguments[i] = sqlArgument!; - } - - return true; - } - } - - #region StartsWith/EndsWith/Contains - - private bool TryTranslateStartsEndsWithContains( - Expression instance, - Expression pattern, - StartsEndsWithContains methodType, - [NotNullWhen(true)] out SqlExpression? translation) - { - if (Visit(instance) is not SqlExpression translatedInstance - || Visit(pattern) is not SqlExpression translatedPattern) - { - translation = null; - return false; - } - - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(translatedInstance, translatedPattern); - - translatedInstance = _sqlExpressionFactory.ApplyTypeMapping(translatedInstance, stringTypeMapping); - translatedPattern = _sqlExpressionFactory.ApplyTypeMapping(translatedPattern, stringTypeMapping); - - switch (translatedPattern) - { - case SqlConstantExpression patternConstant: - { - // The pattern is constant. Aside from null and empty string, we escape all special characters (%, _, \) and send a - // simple LIKE - translation = patternConstant.Value switch - { - 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. - // Note that we don't just return a true constant, since null strings shouldn't match even an empty string - // (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable) - "" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")), - - string s => _sqlExpressionFactory.Like( - translatedInstance, - _sqlExpressionFactory.Constant( - methodType switch - { - StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', - StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), - StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", - - _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) - })), - - _ => throw new UnreachableException() - }; - - return true; - } - - case SqlParameterExpression patternParameter: - { - // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where - // all special characters have been escaped. - var lambda = Expression.Lambda( - Expression.Call( - EscapeLikePatternParameterMethod, - QueryCompilationContext.QueryContextParameter, - Expression.Constant(patternParameter.Name), - Expression.Constant(methodType)), - QueryCompilationContext.QueryContextParameter); - - var escapedPatternParameter = - _queryCompilationContext.RegisterRuntimeParameter( - $"{patternParameter.Name}_{methodType.ToString().ToLower(CultureInfo.InvariantCulture)}", lambda); - - translation = _sqlExpressionFactory.Like( - translatedInstance, - new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping)); - - return true; - } - - default: - // The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped, - // preventing us from translating to LIKE. - switch (methodType) - { - // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare: - // WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern - // This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice. - case StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith: - translation = - _sqlExpressionFactory.Function( - methodType is StartsEndsWithContains.StartsWith ? "left" : "right", - [ - translatedInstance, - _sqlExpressionFactory.Function( - "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. - if (translatedInstance.TypeMapping is { StoreType: not "text" }) - { - translation = _sqlExpressionFactory.Convert(translation, typeof(string), translatedInstance.TypeMapping); - } - - // We compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a simple - // equality would yield true in that case, but we want false. - translation = - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedInstance), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedPattern), - _sqlExpressionFactory.Equal(translation, translatedPattern))); - - break; - - // For Contains, just use strpos and check if the result is greater than 0. Note that strpos returns 1 when the pattern - // is an empty string, just like .NET Contains (so no need to compensate) - case StartsEndsWithContains.Contains: - translation = - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedInstance), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedPattern), - _sqlExpressionFactory.GreaterThan( - _sqlExpressionFactory.Function( - "strpos", [translatedInstance, translatedPattern], nullable: true, - argumentsPropagateNullability: [true, true], typeof(int)), - _sqlExpressionFactory.Constant(0)))); - break; - - default: - throw new UnreachableException(); - } - - return 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. - /// - public static string? ConstructLikePatternParameter( - QueryContext queryContext, - string baseParameterName, - StartsEndsWithContains methodType) - => queryContext.Parameters[baseParameterName] switch - { - null => null, - - // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns. - // Return % which always matches instead. - "" => "%", - - string s => methodType switch - { - StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', - StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), - StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", - _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) - }, - - _ => 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 - /// 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 - } - - private static bool IsLikeWildChar(char c) - => c is '%' or '_'; - - private static string EscapeLikePattern(string pattern) - { - var builder = new StringBuilder(); - for (var i = 0; i < pattern.Length; i++) - { - var c = pattern[i]; - if (IsLikeWildChar(c) || c == LikeEscapeChar) - { - builder.Append(LikeEscapeChar); - } - - builder.Append(c); - } - - return builder.ToString(); - } - - #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) - { - if (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression) - { - var innerType = unaryExpression.Operand.Type.UnwrapNullableType(); - if (innerType.IsEnum) - { - innerType = Enum.GetUnderlyingType(innerType); - } - - var convertedType = unaryExpression.Type.UnwrapNullableType(); - - if (innerType == convertedType - || (convertedType == typeof(int) - && (innerType == typeof(byte) - || innerType == typeof(sbyte) - || innerType == typeof(char) - || innerType == typeof(short) - || innerType == typeof(ushort)))) - { - return TryRemoveImplicitConvert(unaryExpression.Operand); - } - } - - return expression; - } - - [DebuggerStepThrough] - private static bool TranslationFailed(Expression? original, Expression? translation, out SqlExpression? castTranslation) - { - if (original is not null && !(translation is SqlExpression)) - { - castTranslation = null; - return true; - } - - castTranslation = translation as SqlExpression; - return false; - } - - #endregion Copied from RelationalSqlTranslatingExpressionVisitor -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitorFactory.cs deleted file mode 100644 index 8c8319eecd..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitorFactory.cs +++ /dev/null @@ -1,38 +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 NpgsqlSqlTranslatingExpressionVisitorFactory : IRelationalSqlTranslatingExpressionVisitorFactory -{ - private readonly RelationalSqlTranslatingExpressionVisitorDependencies _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 - /// 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 NpgsqlSqlTranslatingExpressionVisitorFactory( - RelationalSqlTranslatingExpressionVisitorDependencies dependencies) - { - _dependencies = 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 - /// 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 RelationalSqlTranslatingExpressionVisitor Create( - QueryCompilationContext queryCompilationContext, - QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) - => new NpgsqlSqlTranslatingExpressionVisitor( - _dependencies, - queryCompilationContext, - queryableMethodTranslatingExpressionVisitor); -} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTreePruner.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTreePruner.cs deleted file mode 100644 index bed46f91f1..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTreePruner.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index 62a6444ec4..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlTypeMappingPostprocessor.cs +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index b4a1d578c3..0000000000 --- a/src/EFCore.PG/Query/Internal/NpgsqlUnnestPostprocessor.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; - -/// -/// Locates instances of in the tree and prunes the WITH ORDINALITY clause from them if the -/// ordinality column isn't referenced anywhere. -/// -/// -/// 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 NpgsqlUnnestPostprocessor : 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. - /// - [return: NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - { - switch (expression) - { - case ShapedQueryExpression shapedQueryExpression: - 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); 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(IsOrdinalityColumn)) - { - if (newTables is null) - { - newTables = new TableExpressionBase[selectExpression.Tables.Count]; - - for (var j = 0; j < i; j++) - { - newTables[j] = selectExpression.Tables[j]; - } - } - - var newUnnest = new PgUnnestExpression(unnest.Alias, unnest.Array, unnest.ColumnName, withOrdinality: false); - - newTables[i] = table switch - { - JoinExpressionBase j => j.Update(newUnnest), - 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( - newTables, - selectExpression.Predicate, - selectExpression.GroupBy, - selectExpression.Having, - selectExpression.Projection, - orderings, - selectExpression.Offset, - selectExpression.Limit)); - } - - default: - return base.Visit(expression); - } - } -} diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs deleted file mode 100644 index 3131ed4470..0000000000 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ /dev/null @@ -1,1059 +0,0 @@ -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; - -/// -public class NpgsqlSqlExpressionFactory : SqlExpressionFactory -{ - private readonly NpgsqlTypeMappingSource _typeMappingSource; - private readonly RelationalTypeMapping _boolTypeMapping; - - private static Type? _nodaTimeDurationType; - private static Type? _nodaTimePeriodType; - - /// - /// Creates a new instance of the class. - /// - /// Parameter object containing dependencies for this class. - public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) - : base(dependencies) - { - _typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource; - _boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!; - } - - #region Expression factory methods - - /// - /// Creates a new , corresponding to the PostgreSQL-specific ~ operator. - /// - public virtual PgRegexMatchExpression RegexMatch( - SqlExpression match, - SqlExpression pattern, - RegexOptions options) - => (PgRegexMatchExpression)ApplyDefaultTypeMapping(new PgRegexMatchExpression(match, pattern, options, null)); - - /// - /// Creates a new , corresponding to the PostgreSQL-specific = ANY operator. - /// - public virtual PgAnyExpression Any( - SqlExpression item, - SqlExpression array, - PgAnyOperatorType operatorType) - => (PgAnyExpression)ApplyDefaultTypeMapping(new PgAnyExpression(item, array, operatorType, null)); - - /// - /// Creates a new , corresponding to the PostgreSQL-specific LIKE ALL operator. - /// - public virtual PgAllExpression All( - SqlExpression item, - SqlExpression array, - PgAllOperatorType operatorType) - => (PgAllExpression)ApplyDefaultTypeMapping(new PgAllExpression(item, array, operatorType, null)); - - /// - /// Creates a new , corresponding to the PostgreSQL-specific array subscripting operator. - /// - public virtual PgArrayIndexExpression ArrayIndex( - SqlExpression array, - SqlExpression index, - bool nullable, - RelationalTypeMapping? typeMapping = null) - { - if (!array.Type.TryGetElementType(out var elementType)) - { - throw new ArgumentException("Array expression must be of an array or List<> type", nameof(array)); - } - - return (PgArrayIndexExpression)ApplyTypeMapping( - new PgArrayIndexExpression(array, index, nullable, elementType, typeMapping: null), - typeMapping); - } - - /// - /// Creates a new , corresponding to the PostgreSQL-specific array subscripting operator. - /// - public virtual PgArraySliceExpression ArraySlice( - SqlExpression array, - SqlExpression? lowerBound, - SqlExpression? upperBound, - bool nullable, - RelationalTypeMapping? typeMapping = null) - => (PgArraySliceExpression)ApplyTypeMapping( - new PgArraySliceExpression(array, lowerBound, upperBound, nullable, array.Type, typeMapping: null), - typeMapping); - - /// - /// Creates a new , for converting a timestamp to UTC. - /// - public virtual AtTimeZoneExpression AtUtc( - SqlExpression timestamp, - RelationalTypeMapping? typeMapping = null) - => AtTimeZone(timestamp, Constant("UTC"), timestamp.Type); - - /// - /// Creates a new , for converting a timestamp to another time zone. - /// - public virtual AtTimeZoneExpression AtTimeZone( - SqlExpression timestamp, - SqlExpression timeZone, - Type type, - RelationalTypeMapping? typeMapping = null) - { - if (typeMapping is null) - { - // PostgreSQL AT TIME ZONE flips the given type from timestamptz to timestamp and vice versa - // See https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-ZONECONVERT - typeMapping = timestamp.TypeMapping ?? _typeMappingSource.FindMapping(timestamp.Type, Dependencies.Model)!; - var storeType = typeMapping.StoreType; - - typeMapping = storeType.StartsWith("timestamp with time zone", StringComparison.Ordinal) - || storeType.StartsWith("timestamptz", StringComparison.Ordinal) - ? _typeMappingSource.FindMapping("timestamp without time zone")! - : storeType.StartsWith("timestamp without time zone", StringComparison.Ordinal) - || storeType.StartsWith("timestamp", StringComparison.Ordinal) - ? _typeMappingSource.FindMapping("timestamp with time zone")! - : throw new ArgumentException( - $"timestamp argument to AtTimeZone had unknown store type {storeType}", nameof(timestamp)); - } - - return new AtTimeZoneExpression( - ApplyDefaultTypeMapping(timestamp), - ApplyDefaultTypeMapping(timeZone), - type, - typeMapping); - } - - /// - /// Creates a new , for performing a PostgreSQL-specific case-insensitive string match - /// (ILIKE). - /// - public virtual PgILikeExpression ILike( - SqlExpression match, - SqlExpression pattern, - SqlExpression? escapeChar = null) - => (PgILikeExpression)ApplyDefaultTypeMapping(new PgILikeExpression(match, pattern, escapeChar, null)); - - /// - /// Creates a new , for traversing inside a JSON document. - /// - public virtual PgJsonTraversalExpression JsonTraversal( - SqlExpression expression, - bool returnsText, - Type type, - RelationalTypeMapping? typeMapping = null) - => JsonTraversal(expression, [], returnsText, type, typeMapping); - - /// - /// Creates a new , for traversing inside a JSON document. - /// - public virtual PgJsonTraversalExpression JsonTraversal( - SqlExpression expression, - IEnumerable path, - bool returnsText, - Type type, - RelationalTypeMapping? typeMapping = null) - => new( - ApplyDefaultTypeMapping(expression), - path.Select(ApplyDefaultTypeMapping).ToArray()!, - returnsText, - type, - typeMapping); - - /// - /// Constructs either a , or, if all provided expressions are constants, a single - /// for the entire array. - /// - public virtual SqlExpression NewArrayOrConstant( - IReadOnlyList elements, - Type type, - RelationalTypeMapping? typeMapping = null) - { - var elementType = type.TryGetElementType(typeof(IEnumerable<>)); - if (elementType is null) - { - throw new ArgumentException($"{type.Name} isn't an IEnumerable", nameof(type)); - } - - var newArrayExpression = NewArray(elements, type, typeMapping); - - if (newArrayExpression.Expressions.Any(e => e is not SqlConstantExpression)) - { - return newArrayExpression; - } - - // All elements are constants; extract their values and return an SqlConstantExpression over the array/list - if (type.IsGenericList()) - { - var list = (IList)Activator.CreateInstance(type, elements.Count)!; - var addMethod = type.GetMethod("Add")!; - for (var i = 0; i < elements.Count; i++) - { - addMethod.Invoke(list, [((SqlConstantExpression)newArrayExpression.Expressions[i]).Value]); - } - - return Constant(list, newArrayExpression.TypeMapping); - } - - // We support any arbitrary IEnumerable as the expression type, but only support arrays and Lists as *concrete* types. - // So unless the type was a List (handled above), we return an array constant here. - var array = Array.CreateInstance(elementType, elements.Count); - for (var i = 0; i < elements.Count; i++) - { - array.SetValue(((SqlConstantExpression)newArrayExpression.Expressions[i]).Value, i); - } - - return Constant(array, newArrayExpression.TypeMapping); - } - - /// - /// Creates a new , for creating a new PostgreSQL array. - /// - public virtual PgNewArrayExpression NewArray( - IReadOnlyList expressions, - Type type, - RelationalTypeMapping? typeMapping = null) - => (PgNewArrayExpression)ApplyTypeMapping(new PgNewArrayExpression(expressions, type, typeMapping), typeMapping); - - /// - public override SqlExpression? MakeBinary( - ExpressionType operatorType, - SqlExpression left, - SqlExpression right, - RelationalTypeMapping? typeMapping, - SqlExpression? existingExpr = null) - { - switch (operatorType) - { - case ExpressionType.Subtract - when left.Type == typeof(DateTime) && right.Type == typeof(DateTime) - || left.Type == typeof(DateTimeOffset) && right.Type == typeof(DateTimeOffset) - || left.Type == typeof(TimeOnly) && right.Type == typeof(TimeOnly): - { - return (SqlBinaryExpression)ApplyTypeMapping( - new SqlBinaryExpression(ExpressionType.Subtract, left, right, typeof(TimeSpan), null), typeMapping); - } - - case ExpressionType.Subtract - when left.Type.FullName == "NodaTime.Instant" && right.Type.FullName == "NodaTime.Instant" - || left.Type.FullName == "NodaTime.ZonedDateTime" && right.Type.FullName == "NodaTime.ZonedDateTime": - { - _nodaTimeDurationType ??= left.Type.Assembly.GetType("NodaTime.Duration"); - return (SqlBinaryExpression)ApplyTypeMapping( - new SqlBinaryExpression(ExpressionType.Subtract, left, right, _nodaTimeDurationType!, null), typeMapping); - } - - case ExpressionType.Subtract - when left.Type.FullName == "NodaTime.LocalDateTime" && right.Type.FullName == "NodaTime.LocalDateTime" - || left.Type.FullName == "NodaTime.LocalTime" && right.Type.FullName == "NodaTime.LocalTime": - { - _nodaTimePeriodType ??= left.Type.Assembly.GetType("NodaTime.Period"); - return (SqlBinaryExpression)ApplyTypeMapping( - new SqlBinaryExpression(ExpressionType.Subtract, left, right, _nodaTimePeriodType!, null), typeMapping); - } - - case ExpressionType.Subtract - when left.Type.FullName == "NodaTime.LocalDate" && right.Type.FullName == "NodaTime.LocalDate": - { - return (SqlBinaryExpression)ApplyTypeMapping( - new SqlBinaryExpression(ExpressionType.Subtract, left, right, typeof(int), null), typeMapping); - } - } - - return base.MakeBinary(operatorType, left, right, typeMapping, existingExpr); - } - - /// - /// Creates a new with the given arguments. - /// - /// An representing SQL unary operator. - /// The left operand of binary operation. - /// The right operand of binary operation. - /// A type mapping to be assigned to the created expression. - /// A with the given arguments. - public virtual SqlExpression MakePostgresBinary( - PgExpressionType operatorType, - SqlExpression left, - SqlExpression right, - RelationalTypeMapping? typeMapping = null) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - var returnType = left.Type; - switch (operatorType) - { - case PgExpressionType.Contains: - case PgExpressionType.ContainedBy: - case PgExpressionType.Overlaps: - case PgExpressionType.NetworkContainedByOrEqual: - case PgExpressionType.NetworkContainsOrEqual: - case PgExpressionType.NetworkContainsOrContainedBy: - case PgExpressionType.RangeIsStrictlyLeftOf: - case PgExpressionType.RangeIsStrictlyRightOf: - case PgExpressionType.RangeDoesNotExtendRightOf: - case PgExpressionType.RangeDoesNotExtendLeftOf: - case PgExpressionType.RangeIsAdjacentTo: - case PgExpressionType.TextSearchMatch: - case PgExpressionType.JsonExists: - case PgExpressionType.JsonExistsAny: - case PgExpressionType.JsonExistsAll: - returnType = typeof(bool); - break; - - case PgExpressionType.Distance: - returnType = typeof(double); - break; - } - - return (PgBinaryExpression)ApplyTypeMapping( - new PgBinaryExpression(operatorType, left, right, returnType, null), typeMapping); - } - - /// - /// Creates a new , for checking whether one value contains another. - /// - public virtual SqlExpression Contains(SqlExpression left, SqlExpression right) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - return MakePostgresBinary(PgExpressionType.Contains, left, right); - } - - /// - /// Creates a new , for checking whether one value is contained by another. - /// - public virtual SqlExpression ContainedBy(SqlExpression left, SqlExpression right) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - return MakePostgresBinary(PgExpressionType.ContainedBy, left, right); - } - - /// - /// Creates a new , for checking whether one value overlaps with another. - /// - public virtual SqlExpression Overlaps(SqlExpression left, SqlExpression right) - { - Check.NotNull(left, nameof(left)); - Check.NotNull(right, nameof(right)); - - return MakePostgresBinary(PgExpressionType.Overlaps, left, right); - } - - /// - /// Creates a new for a PostgreSQL aggregate function call.. - /// - public virtual PgFunctionExpression AggregateFunction( - string name, - IEnumerable arguments, - EnumerableExpression aggregateEnumerableExpression, - bool nullable, - IEnumerable argumentsPropagateNullability, - Type returnType, - RelationalTypeMapping? typeMapping = null) - { - var typeMappedArguments = new List(); - - foreach (var argument in arguments) - { - typeMappedArguments.Add(ApplyDefaultTypeMapping(argument)); - } - - return new PgFunctionExpression( - name, - typeMappedArguments, - argumentNames: null, - argumentSeparators: null, - aggregateEnumerableExpression.IsDistinct, - aggregateEnumerableExpression.Predicate, - aggregateEnumerableExpression.Orderings, - nullable: nullable, - argumentsPropagateNullability: argumentsPropagateNullability, type: returnType, typeMapping: typeMapping); - } - - #endregion Expression factory methods - - /// - [return: NotNullIfNotNull("sqlExpression")] - public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) - { - if (sqlExpression is not null && sqlExpression.TypeMapping is null) - { - sqlExpression = sqlExpression switch - { - SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping), - - // PostgreSQL-specific expression types - PgAnyExpression e => ApplyTypeMappingOnAny(e), - PgAllExpression e => ApplyTypeMappingOnAll(e), - PgArrayIndexExpression e => ApplyTypeMappingOnArrayIndex(e, typeMapping), - PgArraySliceExpression e => ApplyTypeMappingOnArraySlice(e, typeMapping), - PgBinaryExpression e => ApplyTypeMappingOnPostgresBinary(e, typeMapping), - PgFunctionExpression e => e.ApplyTypeMapping(typeMapping), - PgILikeExpression e => ApplyTypeMappingOnILike(e), - PgNewArrayExpression e => ApplyTypeMappingOnNewArray(e, typeMapping), - PgRegexMatchExpression e => ApplyTypeMappingOnRegexMatch(e), - PgRowValueExpression e => ApplyTypeMappingOnRowValue(e, typeMapping), - - _ => base.ApplyTypeMapping(sqlExpression, typeMapping) - }; - } - - if (!NpgsqlTypeMappingSource.LegacyTimestampBehavior - && (typeMapping is NpgsqlTimestampTypeMapping && sqlExpression?.TypeMapping is NpgsqlTimestampTzTypeMapping - || typeMapping is NpgsqlTimestampTzTypeMapping && sqlExpression?.TypeMapping is NpgsqlTimestampTypeMapping)) - { - throw new NotSupportedException( - "Cannot apply binary operation on types 'timestamp with time zone' and 'timestamp without time zone', convert one of the operands first."); - } - - return sqlExpression; - } - - private SqlBinaryExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression binary, RelationalTypeMapping? typeMapping) - { - var (left, right) = (binary.Left, binary.Right); - - // The default SqlExpressionFactory behavior is to assume that the two added operands have the same type, - // and so to infer one side's mapping from the other if needed. Here we take care of some heterogeneous - // operand cases where this doesn't work: - // * Period + Period (???) - - switch (binary.OperatorType) - { - // DateTime + TimeSpan => DateTime - // DateTimeOffset + TimeSpan => DateTimeOffset - // TimeOnly + TimeSpan => TimeOnly - case ExpressionType.Add or ExpressionType.Subtract - when right.Type == typeof(TimeSpan) - && (left.Type == typeof(DateTime) || left.Type == typeof(DateTimeOffset) || left.Type == typeof(TimeOnly)) - || right.Type == typeof(int) && left.Type == typeof(DateOnly) - || right.Type.FullName == "NodaTime.Period" - && left.Type.FullName is "NodaTime.LocalDateTime" or "NodaTime.LocalDate" or "NodaTime.LocalTime" - || right.Type.FullName == "NodaTime.Duration" - && left.Type.FullName is "NodaTime.Instant" or "NodaTime.ZonedDateTime": - { - var newLeft = ApplyTypeMapping(left, typeMapping); - var newRight = ApplyDefaultTypeMapping(right); - return new SqlBinaryExpression(binary.OperatorType, newLeft, newRight, binary.Type, newLeft.TypeMapping); - } - - // DateTime - DateTime => TimeSpan - // DateTimeOffset - DateTimeOffset => TimeSpan - // DateOnly - DateOnly => TimeSpan - // TimeOnly - TimeOnly => TimeSpan - // Instant - Instant => Duration - // LocalDateTime - LocalDateTime => int (days) - case ExpressionType.Subtract - when left.Type == typeof(DateTime) && right.Type == typeof(DateTime) - || left.Type == typeof(DateTimeOffset) && right.Type == typeof(DateTimeOffset) - || left.Type == typeof(DateOnly) && right.Type == typeof(DateOnly) - || left.Type == typeof(TimeOnly) && right.Type == typeof(TimeOnly) - || left.Type.FullName == "NodaTime.Instant" && right.Type.FullName == "NodaTime.Instant" - || left.Type.FullName == "NodaTime.LocalDateTime" && right.Type.FullName == "NodaTime.LocalDateTime" - || left.Type.FullName == "NodaTime.ZonedDateTime" && right.Type.FullName == "NodaTime.ZonedDateTime" - || left.Type.FullName == "NodaTime.LocalDate" && right.Type.FullName == "NodaTime.LocalDate" - || left.Type.FullName == "NodaTime.LocalTime" && right.Type.FullName == "NodaTime.LocalTime": - { - var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right); - - return new SqlBinaryExpression( - ExpressionType.Subtract, - ApplyTypeMapping(left, inferredTypeMapping), - ApplyTypeMapping(right, inferredTypeMapping), - 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. - if (IsComparison(binary.OperatorType) - && TryGetRowValueValues(binary.Left, out var leftValues) - && TryGetRowValueValues(binary.Right, out var rightValues)) - { - if (leftValues.Count != rightValues.Count) - { - throw new ArgumentException(NpgsqlStrings.RowValueComparisonRequiresTuplesOfSameLength); - } - - var count = leftValues.Count; - var updatedLeftValues = new SqlExpression[count]; - var updatedRightValues = new SqlExpression[count]; - - for (var i = 0; i < count; i++) - { - var updatedElementBinaryExpression = MakeBinary(binary.OperatorType, leftValues[i], rightValues[i], typeMapping: null)!; - - 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 - // SqlConstantExpression. This is because each value in the row value needs to have its type mapping. - binary = new SqlBinaryExpression( - binary.OperatorType, - new PgRowValueExpression(updatedLeftValues, binary.Left.Type), - new PgRowValueExpression(updatedRightValues, binary.Right.Type), - binary.Type, - binary.TypeMapping); - } - - return (SqlBinaryExpression)base.ApplyTypeMapping(binary, typeMapping); - - static bool IsComparison(ExpressionType expressionType) - { - switch (expressionType) - { - case ExpressionType.Equal: - case ExpressionType.NotEqual: - case ExpressionType.GreaterThan: - case ExpressionType.LessThan: - case ExpressionType.GreaterThanOrEqual: - case ExpressionType.LessThanOrEqual: - return true; - default: - return false; - } - } - - bool TryGetRowValueValues(SqlExpression e, [NotNullWhen(true)] out IReadOnlyList? values) - { - switch (e) - { - case PgRowValueExpression rowValueExpression: - values = rowValueExpression.Values; - return true; - - case SqlConstantExpression { Value : ITuple constantTuple }: - var v = new SqlExpression[constantTuple.Length]; - - for (var i = 0; i < v.Length; i++) - { - v[i] = Constant(constantTuple[i], typeof(object)); - } - - values = v; - return true; - - default: - values = null; - return false; - } - } - } - - private SqlExpression ApplyTypeMappingOnRegexMatch(PgRegexMatchExpression pgRegexMatchExpression) - { - var inferredTypeMapping = ExpressionExtensions.InferTypeMapping( - pgRegexMatchExpression.Match, pgRegexMatchExpression.Pattern) - ?? _typeMappingSource.FindMapping(pgRegexMatchExpression.Match.Type, Dependencies.Model); - - return new PgRegexMatchExpression( - ApplyTypeMapping(pgRegexMatchExpression.Match, inferredTypeMapping), - ApplyTypeMapping(pgRegexMatchExpression.Pattern, inferredTypeMapping), - pgRegexMatchExpression.Options, - _boolTypeMapping); - } - - private SqlExpression ApplyTypeMappingOnRowValue( - PgRowValueExpression pgRowValueExpression, - RelationalTypeMapping? typeMapping) - { - // If the row value is in a binary expression (e.g. a comparison, (a, b) > (5, 6)), we have special type inference code - // to infer from the other row value in ApplyTypeMappingOnSqlBinary. - // If we're here, that means that no such inference can happen, and we just use the default type mappings. - var updatedValues = new SqlExpression[pgRowValueExpression.Values.Count]; - - for (var i = 0; i < updatedValues.Length; i++) - { - updatedValues[i] = ApplyDefaultTypeMapping(pgRowValueExpression.Values[i]); - } - - return new PgRowValueExpression(updatedValues, pgRowValueExpression.Type, typeMapping); - } - - private SqlExpression ApplyTypeMappingOnAny(PgAnyExpression pgAnyExpression) - { - var (item, array) = ApplyTypeMappingsOnItemAndArray(pgAnyExpression.Item, pgAnyExpression.Array); - return new PgAnyExpression(item, array, pgAnyExpression.OperatorType, _boolTypeMapping); - } - - private SqlExpression ApplyTypeMappingOnAll(PgAllExpression pgAllExpression) - { - var (item, array) = ApplyTypeMappingsOnItemAndArray(pgAllExpression.Item, pgAllExpression.Array); - return new PgAllExpression(item, array, pgAllExpression.OperatorType, _boolTypeMapping); - } - - internal (SqlExpression, SqlExpression) ApplyTypeMappingsOnItemAndArray(SqlExpression itemExpression, SqlExpression arrayExpression) - { - // Attempt type inference either from the operand to the array or the other way around - var arrayMapping = arrayExpression.TypeMapping; - - var itemMapping = - itemExpression.TypeMapping - // Unwrap convert-to-object nodes - these get added for object[].Contains(x) - ?? (itemExpression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } unary && unary.Type == typeof(object) - ? unary.Operand.TypeMapping - : null) - // If we couldn't find a type mapping on the item, try inferring it from the array - ?? (RelationalTypeMapping?)arrayMapping?.ElementTypeMapping - ?? _typeMappingSource.FindMapping(itemExpression.Type, Dependencies.Model); - - if (itemMapping is null) - { - throw new InvalidOperationException("Couldn't find element type mapping when applying item/array mappings"); - } - - // If the array's type mapping isn't provided (parameter/constant), attempt to infer it from the item. - if (arrayMapping is null) - { - if (itemMapping.Converter is not null) - { - // If the item mapping has a value converter, construct an array mapping directly over it - this will build the - // corresponding array type converter. - arrayMapping = _typeMappingSource.FindMapping(arrayExpression.Type, Dependencies.Model, itemMapping); - } - else - { - // No value converter on the item mapping - just try to look up an array mapping based on the item type. - // Special-case arrays of objects, not taking the array CLR type into account in the lookup (it would never succeed). - // Note that we provide both the array CLR type *and* an array store type constructed from the element's store type. - // If we use only the array CLR type, byte[] will yield bytea which we don't want. - arrayMapping = arrayExpression.Type.TryGetSequenceType() == typeof(object) - ? _typeMappingSource.FindMapping(itemMapping.StoreType + "[]") - : _typeMappingSource.FindMapping(arrayExpression.Type, itemMapping.StoreType + "[]"); - } - - if (arrayMapping is null) - { - throw new InvalidOperationException("Couldn't find array type mapping when applying item/array mappings"); - } - } - - return (ApplyTypeMapping(itemExpression, itemMapping), ApplyTypeMapping(arrayExpression, arrayMapping)); - } - - private SqlExpression ApplyTypeMappingOnArrayIndex( - PgArrayIndexExpression pgArrayIndexExpression, - RelationalTypeMapping? typeMapping) - { - // 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.ClrType, typeMapping), pgArrayIndexExpression.Array) - : (null, ApplyDefaultTypeMapping(pgArrayIndexExpression.Array)); - - return new PgArrayIndexExpression( - array, - ApplyDefaultTypeMapping(pgArrayIndexExpression.Index), - pgArrayIndexExpression.IsNullable, - pgArrayIndexExpression.Type, - // If the array has a type mapping (i.e. column), prefer that just like we prefer column mappings in general - pgArrayIndexExpression.Array.TypeMapping is NpgsqlArrayTypeMapping arrayMapping - ? arrayMapping.ElementTypeMapping - : typeMapping - ?? _typeMappingSource.FindMapping(pgArrayIndexExpression.Type, Dependencies.Model)); - } - - private SqlExpression ApplyTypeMappingOnArraySlice( - PgArraySliceExpression slice, - RelationalTypeMapping? typeMapping) - { - // If the slice operand has a type mapping, that bubbles up (slice is just a view over that). Otherwise apply the external type - // mapping down. The bounds are always ints and don't participate in any inference. - - // 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 = ApplyTypeMapping(slice.Array, typeMapping); - - return new PgArraySliceExpression( - array, - slice.LowerBound is null ? null : ApplyDefaultTypeMapping(slice.LowerBound), - slice.UpperBound is null ? null : ApplyDefaultTypeMapping(slice.UpperBound), - slice.IsNullable, - slice.Type, - array.TypeMapping); - } - - private SqlExpression ApplyTypeMappingOnILike(PgILikeExpression ilikeExpression) - { - var inferredTypeMapping = (ilikeExpression.EscapeChar is null - ? ExpressionExtensions.InferTypeMapping( - ilikeExpression.Match, ilikeExpression.Pattern) - : ExpressionExtensions.InferTypeMapping( - ilikeExpression.Match, ilikeExpression.Pattern, - ilikeExpression.EscapeChar)) - ?? _typeMappingSource.FindMapping(ilikeExpression.Match.Type, Dependencies.Model); - - return new PgILikeExpression( - ApplyTypeMapping(ilikeExpression.Match, inferredTypeMapping), - ApplyTypeMapping(ilikeExpression.Pattern, inferredTypeMapping), - ApplyTypeMapping(ilikeExpression.EscapeChar, inferredTypeMapping), - _boolTypeMapping); - } - - private SqlExpression ApplyTypeMappingOnPostgresBinary( - PgBinaryExpression pgBinaryExpression, - RelationalTypeMapping? typeMapping) - { - var (left, right) = (pgBinaryExpression.Left, pgBinaryExpression.Right); - - Type resultType; - RelationalTypeMapping? resultTypeMapping = null; - RelationalTypeMapping? inferredTypeMapping; - var operatorType = pgBinaryExpression.OperatorType; - switch (operatorType) - { - case PgExpressionType.Overlaps: - case PgExpressionType.Contains: - case PgExpressionType.ContainedBy: - case PgExpressionType.RangeIsStrictlyLeftOf: - case PgExpressionType.RangeIsStrictlyRightOf: - case PgExpressionType.RangeDoesNotExtendRightOf: - case PgExpressionType.RangeDoesNotExtendLeftOf: - case PgExpressionType.RangeIsAdjacentTo: - { - resultType = typeof(bool); - resultTypeMapping = _boolTypeMapping; - - // Simple case: we have the same CLR type on both sides, or we have an array on either side - // (e.g. overlap/intersect between two arrays); note that different CLR types may be mapped to arrays on the two sides - // (e.g. int[] and List) - if (left.Type == right.Type - || left.TypeMapping is NpgsqlArrayTypeMapping - || right.TypeMapping is NpgsqlArrayTypeMapping) - { - inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right); - break; - } - - // Multirange and range, cidr and ip - cases of different types where one contains the other. - // We need fancier type mapping inference. - SqlExpression newLeft, newRight; - - if (operatorType == PgExpressionType.ContainedBy) - { - (newRight, newLeft) = InferContainmentMappings(right, left); - } - else - { - (newLeft, newRight) = InferContainmentMappings(left, right); - } - - return new PgBinaryExpression(operatorType, newLeft, newRight, resultType, resultTypeMapping); - } - - case PgExpressionType.NetworkContainedByOrEqual: - case PgExpressionType.NetworkContainsOrEqual: - case PgExpressionType.NetworkContainsOrContainedBy: - case PgExpressionType.TextSearchMatch: - case PgExpressionType.JsonExists: - case PgExpressionType.JsonExistsAny: - case PgExpressionType.JsonExistsAll: - { - // TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are - // based on operator type? - return new PgBinaryExpression( - operatorType, - ApplyDefaultTypeMapping(left), - ApplyDefaultTypeMapping(right), - typeof(bool), - _boolTypeMapping); - } - - case PgExpressionType.RangeUnion: - case PgExpressionType.RangeIntersect: - case PgExpressionType.RangeExcept: - case PgExpressionType.TextSearchAnd: - case PgExpressionType.TextSearchOr: - { - inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right); - resultType = inferredTypeMapping?.ClrType ?? left.Type; - resultTypeMapping = inferredTypeMapping; - break; - } - - case PgExpressionType.Distance: - { - inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right); - - resultType = inferredTypeMapping?.StoreTypeNameBase switch - { - "geometry" or "geography" => typeof(double), - - "cube" => typeof(double), - - "date" => typeof(int), - - "interval" when left.Type.FullName is "NodaTime.Period" or "NodaTime.Duration" - => _nodaTimePeriodType ??= left.Type.Assembly.GetType("NodaTime.Period")!, - "interval" => typeof(TimeSpan), - - "timestamp" or "timestamptz" or "timestamp with time zone" or "timestamp without time zone" - when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "NodaTime.ZonedDateTime" - => _nodaTimePeriodType ??= left.Type.Assembly.GetType("NodaTime.Period")!, - "timestamp" or "timestamptz" or "timestamp with time zone" or "timestamp without time zone" - => typeof(TimeSpan), - - null => throw new InvalidOperationException("No inferred type mapping for distance operator"), - _ => throw new InvalidOperationException( - $"PostgreSQL type '{inferredTypeMapping.StoreTypeNameBase}' isn't supported with the distance operator") - }; - break; - } - - default: - throw new InvalidOperationException( - $"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}"); - } - - return new PgBinaryExpression( - operatorType, - ApplyTypeMapping(left, inferredTypeMapping), - ApplyTypeMapping(right, inferredTypeMapping), - resultType, - resultTypeMapping ?? _typeMappingSource.FindMapping(resultType)); - - (SqlExpression, SqlExpression) InferContainmentMappings(SqlExpression container, SqlExpression containee) - { - Debug.Assert( - container.Type != containee.Type, - "This method isn't meant for identical types, where type mapping inference is much simpler"); - - // Attempt type inference either from the container or from the containee - var containerMapping = container.TypeMapping; - var containeeMapping = containee.TypeMapping; - - if (containeeMapping is null) - { - // If we couldn't find a type mapping on the containee, try inferring it from the container - containeeMapping = containerMapping switch - { - NpgsqlRangeTypeMapping rangeTypeMapping => rangeTypeMapping.SubtypeMapping, - NpgsqlMultirangeTypeMapping multirangeTypeMapping - => containee.Type.IsGenericType && containee.Type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>) - ? multirangeTypeMapping.RangeMapping - : multirangeTypeMapping.SubtypeMapping, - _ => null - }; - - // Apply the inferred mapping to the containee, or fall back to the default type mapping - if (containeeMapping is not null) - { - containee = ApplyTypeMapping(containee, containeeMapping); - } - else - { - containee = ApplyDefaultTypeMapping(containee); - containeeMapping = containee.TypeMapping; - - if (containeeMapping is null) - { - throw new InvalidOperationException( - "Couldn't find containee type mapping when applying container/containee mappings"); - } - } - } - - // If the container's type mapping isn't provided (parameter/constant), attempt to infer it from the item. - if (containerMapping is null) - { - // TODO: FindContainerMapping currently works for range/multirange only, may want to extend it to other types - // (e.g. IP address containment) - containerMapping = _typeMappingSource.FindContainerMapping(container.Type, containeeMapping, Dependencies.Model); - - // Apply the inferred mapping to the container, or fall back to the default type mapping - if (containerMapping is not null) - { - container = ApplyTypeMapping(container, containerMapping); - } - else - { - container = ApplyDefaultTypeMapping(container); - - if (container.TypeMapping is null) - { - throw new InvalidOperationException( - "Couldn't find container type mapping when applying container/containee mappings"); - } - } - } - - return (ApplyTypeMapping(container, containerMapping), ApplyTypeMapping(containee, containeeMapping)); - } - } - - private SqlExpression ApplyTypeMappingOnNewArray( - PgNewArrayExpression pgNewArrayExpression, - RelationalTypeMapping? typeMapping) - { - var arrayTypeMapping = typeMapping as NpgsqlArrayTypeMapping; - if (arrayTypeMapping is null && typeMapping is not null) - { - throw new ArgumentException($"Type mapping {typeMapping.GetType().Name} isn't an {nameof(NpgsqlArrayTypeMapping)}"); - } - - RelationalTypeMapping? elementTypeMapping = null; - - // First, loop over the expressions to infer the array's type mapping (if not provided), and to make - // sure we don't have heterogeneous store types. - foreach (var expression in pgNewArrayExpression.Expressions) - { - if (expression.TypeMapping is not { } expressionTypeMapping) - { - continue; - } - - if (elementTypeMapping is null) - { - elementTypeMapping = expressionTypeMapping; - } - else if (expressionTypeMapping.StoreType != elementTypeMapping.StoreType) - { - // We have two heterogeneous store types in the array. - // We allow this when they have the same base type but differing facets (e.g. varchar(10) and varchar(15)), in which case - // we cast up. We also manually take care of some special cases (e.g. text and varchar(10) -> text). - // This is a hacky solution until a full type compatibility chart is implemented - // (https://github.com/dotnet/efcore/issues/15586) - if (expressionTypeMapping.StoreTypeNameBase == elementTypeMapping.StoreTypeNameBase) - { - if (expressionTypeMapping.Size is not null && elementTypeMapping.Size is not null) - { - var size = Math.Max(expressionTypeMapping.Size.Value, elementTypeMapping.Size.Value); - - elementTypeMapping = _typeMappingSource.FindMapping($"{expressionTypeMapping.StoreTypeNameBase}({size})"); - } - else if (expressionTypeMapping.Precision is not null - && elementTypeMapping.Precision is not null - && expressionTypeMapping.Scale is not null - && elementTypeMapping.Scale is not null) - { - var precision = Math.Max(expressionTypeMapping.Precision.Value, elementTypeMapping.Precision.Value); - var scale = Math.Max(expressionTypeMapping.Scale.Value, elementTypeMapping.Scale.Value); - - elementTypeMapping = - _typeMappingSource.FindMapping($"{expressionTypeMapping.StoreTypeNameBase}({precision},{scale})"); - } - else if (expressionTypeMapping.Precision is not null && elementTypeMapping.Precision is not null) - { - var precision = Math.Max(expressionTypeMapping.Precision.Value, elementTypeMapping.Precision.Value); - - elementTypeMapping = _typeMappingSource.FindMapping($"{expressionTypeMapping.StoreTypeNameBase}({precision})"); - } - } - else if (expressionTypeMapping.StoreType == "text" && IsTextualTypeMapping(elementTypeMapping)) - { - elementTypeMapping = expressionTypeMapping; - } - else if (elementTypeMapping.StoreType == "text" && IsTextualTypeMapping(expressionTypeMapping)) - { - // elementTypeMapping is already "text" - } - else - { - throw new InvalidOperationException( - NpgsqlStrings.HeterogeneousTypesInNewArray( - elementTypeMapping.StoreType, expressionTypeMapping.StoreType)); - } - - static bool IsTextualTypeMapping(RelationalTypeMapping mapping) - => mapping.StoreTypeNameBase is "varchar" or "char" or "character varying" or "character" or "text"; - } - } - - // None of the array's expressions had a type mapping (i.e. no columns, only parameters/constants) - // Use the type mapping given externally - if (elementTypeMapping is null) - { - // No type mapping could be inferred from the expressions, nor was one given from the outside - - // we have no type mapping... Just return the original expression, which has no type mapping and will fail translation. - if (arrayTypeMapping is null) - { - return pgNewArrayExpression; - } - - elementTypeMapping = arrayTypeMapping.ElementTypeMapping; - } - else - { - // An element type mapping was successfully inferred from one of the expressions (there was a column). - // Infer the array's type mapping from it. - arrayTypeMapping = (NpgsqlArrayTypeMapping?)_typeMappingSource.FindMapping( - pgNewArrayExpression.Type, - elementTypeMapping.StoreType + "[]"); - - // If the array's CLR type doesn't match the type mapping inferred from the element (e.g. CLR object[] with up-casted - // elements). Just return the original expression, which has no type mapping and will fail translation. - if (arrayTypeMapping is null) - { - return pgNewArrayExpression; - } - } - - // Now go over all expressions and apply the inferred element type mapping - List? newExpressions = null; - for (var i = 0; i < pgNewArrayExpression.Expressions.Count; i++) - { - var expression = pgNewArrayExpression.Expressions[i]; - var newExpression = ApplyTypeMapping(expression, elementTypeMapping); - if (newExpression != expression && newExpressions is null) - { - newExpressions = []; - for (var j = 0; j < i; j++) - { - newExpressions.Add(pgNewArrayExpression.Expressions[j]); - } - } - - newExpressions?.Add(newExpression); - } - - return new PgNewArrayExpression( - newExpressions ?? pgNewArrayExpression.Expressions, - pgNewArrayExpression.Type, arrayTypeMapping); - } - - /// - /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, - /// just increment it. Otherwise, append a +1 in the SQL. - /// - public virtual SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) - => expression is SqlConstantExpression constant - ? Constant(System.Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) - : Add(expression, Constant(1)); -} diff --git a/src/EFCore.PG/README.md b/src/EFCore.PG/README.md deleted file mode 100644 index a595c918e9..0000000000 --- a/src/EFCore.PG/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Npgsql Entity Framework Core provider for PostgreSQL - -Npgsql.EntityFrameworkCore.PostgreSQL is the open source EF Core provider for PostgreSQL. It allows you to interact with PostgreSQL via the most widely-used .NET O/RM from Microsoft, and use familiar LINQ syntax to express queries. It's built on top of [Npgsql](https://github.com/npgsql/npgsql). - -The provider looks and feels just like any other Entity Framework Core provider. Here's a quick sample to get you started: - -```csharp -await using var ctx = new BlogContext(); -await ctx.Database.EnsureDeletedAsync(); -await ctx.Database.EnsureCreatedAsync(); - -// Insert a Blog -ctx.Blogs.Add(new() { Name = "FooBlog" }); -await ctx.SaveChangesAsync(); - -// Query all blogs who's name starts with F -var fBlogs = await ctx.Blogs.Where(b => b.Name.StartsWith("F")).ToListAsync(); - -public class BlogContext : DbContext -{ - public DbSet Blogs { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(@"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase"); -} - -public class Blog -{ - public int Id { get; set; } - public string Name { get; set; } -} -``` - -Aside from providing general EF Core support for PostgreSQL, the provider also exposes some PostgreSQL-specific capabilities, allowing you to query JSON, array or range columns, as well as many other advanced features. For more information, see the [the Npgsql site](http://www.npgsql.org/efcore/index.html). For information about EF Core in general, see the [EF Core website](https://docs.microsoft.com/ef/core/). - diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs deleted file mode 100644 index 32d0b4dd79..0000000000 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlCodeGenerator.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; - -/// -/// The default code generator for Npgsql. -/// -public class NpgsqlCodeGenerator : ProviderCodeGenerator -{ - private static readonly MethodInfo _useNpgsqlMethodInfo - = typeof(NpgsqlDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod( - nameof(NpgsqlDbContextOptionsBuilderExtensions.UseNpgsql), - typeof(DbContextOptionsBuilder), - typeof(string), - typeof(Action)); - - /// - /// Constructs an instance of the class. - /// - /// The dependencies. - public NpgsqlCodeGenerator(ProviderCodeGeneratorDependencies dependencies) - : base(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 - /// 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 MethodCallCodeFragment GenerateUseProvider( - string connectionString, - MethodCallCodeFragment? providerOptions) - => new( - _useNpgsqlMethodInfo, - providerOptions is null - ? [connectionString] - : [connectionString, new NestedClosureCodeFragment("x", providerOptions)]); -} diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs deleted file mode 100644 index b305a86260..0000000000 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs +++ /dev/null @@ -1,1496 +0,0 @@ -using System.Data; -using System.Data.Common; -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; - -// ReSharper disable StringLiteralTypo - -/// -/// The default database model factory for Npgsql. -/// -public class NpgsqlDatabaseModelFactory : DatabaseModelFactory -{ - #region Fields - - private const string NamePartRegex = """(?:(?:"(?(?:(?:"")|[^"])+)")|(?[^\.\["]+))"""; - - private static readonly Regex SchemaTableNameExtractor = - new( - string.Format( - CultureInfo.InvariantCulture, - @"^{0}(?:\.{1})?$", - string.Format(CultureInfo.InvariantCulture, NamePartRegex, 1), - string.Format(CultureInfo.InvariantCulture, NamePartRegex, 2)), - RegexOptions.Compiled, - TimeSpan.FromMilliseconds(1000.0)); - - private static readonly string[] SerialTypes = ["int2", "int4", "int8"]; - - private readonly IDiagnosticsLogger _logger; - - #endregion - - #region Public surface - - /// - /// Constructs an instance of the class. - /// - public NpgsqlDatabaseModelFactory(IDiagnosticsLogger logger) - { - _logger = Check.NotNull(logger, nameof(logger)); - } - - /// - public override DatabaseModel Create(string connectionString, DatabaseModelFactoryOptions options) - { - Check.NotEmpty(connectionString, nameof(connectionString)); - Check.NotNull(options, nameof(options)); - - using var connection = new NpgsqlConnection(connectionString); - return Create(connection, options); - } - - /// - public override DatabaseModel Create(DbConnection dbConnection, DatabaseModelFactoryOptions options) - { - Check.NotNull(dbConnection, nameof(dbConnection)); - Check.NotNull(options, nameof(options)); - - var databaseModel = new DatabaseModel(); - - var connection = (NpgsqlConnection)dbConnection; - - var connectionStartedOpen = connection.State == ConnectionState.Open; - - if (!connectionStartedOpen) - { - connection.Open(); - } - - try - { - var internalSchemas = "'pg_catalog', 'information_schema'"; - using (var command = new NpgsqlCommand("SELECT version()", connection)) - { - var longVersion = (string)command.ExecuteScalar()!; - if (longVersion.Contains("CockroachDB")) - { - internalSchemas += ", 'crdb_internal'"; - } - } - - databaseModel.DatabaseName = connection.Database; - databaseModel.DefaultSchema = "public"; - - PopulateGlobalDatabaseInfo(connection, databaseModel); - - var schemaList = options.Schemas.ToList(); - var schemaFilter = GenerateSchemaFilter(schemaList); - var tableList = options.Tables.ToList(); - var tableFilter = GenerateTableFilter(tableList.Select(Parse).ToList(), schemaFilter); - - var enums = GetEnums(connection, databaseModel); - - foreach (var table in GetTables(connection, databaseModel, tableFilter, internalSchemas, enums, _logger)) - { - table.Database = databaseModel; - databaseModel.Tables.Add(table); - } - - foreach (var table in databaseModel.Tables) - { - while (table.Columns.Remove(null!)) { } - } - - foreach (var sequence in GetSequences(connection, databaseModel, schemaFilter, _logger)) - { - sequence.Database = databaseModel; - databaseModel.Sequences.Add(sequence); - } - - if (connection.PostgreSqlVersion >= new Version(9, 1)) - { - GetExtensions(connection, databaseModel); - GetCollations(connection, databaseModel, internalSchemas, _logger); - } - - for (var i = 0; i < databaseModel.Tables.Count; i++) - { - var table = databaseModel.Tables[i]; - - // We may have dropped or skipped columns. We load these because constraints take them into - // account when referencing columns, but must now get rid of them before returning - // the database model. - while (table.Columns.Remove(null!)) { } - } - - foreach (var schema in schemaList - .Except(databaseModel.Sequences.Select(s => s.Schema).Concat(databaseModel.Tables.Select(t => t.Schema)))) - { - _logger.MissingSchemaWarning(schema); - } - - foreach (var table in tableList) - { - var (schema, name) = Parse(table); - if (!databaseModel.Tables.Any(t => !string.IsNullOrEmpty(schema) && t.Schema == schema || t.Name == name)) - { - _logger.MissingTableWarning(table); - } - } - - return databaseModel; - } - finally - { - if (!connectionStartedOpen) - { - connection.Close(); - } - } - } - - #endregion - - #region Type information queries - - private static void PopulateGlobalDatabaseInfo(NpgsqlConnection connection, DatabaseModel databaseModel) - { - if (connection.PostgreSqlVersion < new Version(8, 4)) - { - return; - } - - var commandText = """ -SELECT datcollate -FROM pg_database -WHERE datname=current_database() AND datcollate <> (SELECT datcollate FROM pg_database WHERE datname='template1') -"""; - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - if (reader.Read()) - { - databaseModel.Collation = reader.GetString(0); - } - } - - /// - /// Queries the database for defined tables and registers them with the model. - /// - private static IEnumerable GetTables( - NpgsqlConnection connection, - DatabaseModel databaseModel, - Func? tableFilter, - string internalSchemas, - HashSet enums, - IDiagnosticsLogger logger) - { - var filter = tableFilter is not null ? $"AND {tableFilter("ns.nspname", "cls.relname")}" : null; - var commandText = $""" -SELECT - nspname, relname, relkind, description, - {(connection.PostgreSqlVersion >= new Version(8, 2) ? "reloptions" : "'{}'::text[] AS reloptions")} -FROM pg_class AS cls -JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace -LEFT OUTER JOIN pg_description AS des ON des.objoid = cls.oid AND des.objsubid=0 -WHERE - cls.relkind IN ('r', 'v', 'm', 'f') AND - ns.nspname NOT IN ({internalSchemas}) AND - cls.relname <> '{HistoryRepository.DefaultTableName}' AND - -- Exclude tables which are members of PG extensions - NOT EXISTS ( - SELECT 1 FROM pg_depend WHERE - classid=( - SELECT cls.oid - FROM pg_class AS cls - JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace - WHERE relname='pg_class' AND ns.nspname='pg_catalog' - ) AND - objid=cls.oid AND - deptype IN ('e', 'x') - ) - {filter} -"""; - - var tables = new List(); - - using (var command = new NpgsqlCommand(commandText, connection)) - using (var reader = command.ExecuteReader()) - { - while (reader.Read()) - { - var schema = reader.GetValueOrDefault("nspname"); - var name = reader.GetString("relname"); - var type = reader.GetChar("relkind"); - var comment = reader.GetValueOrDefault("description"); - var storageParameters = reader.GetValueOrDefault("reloptions") ?? []; - - var table = type switch - { - 'r' => new DatabaseTable(), - 'f' => new DatabaseTable(), - 'v' => new DatabaseView(), - 'm' => new DatabaseView(), - _ => throw new ArgumentOutOfRangeException($"Unknown relkind '{type}' when scaffolding {DisplayName(schema, name)}") - }; - - table.Database = databaseModel; - table.Name = name; - table.Schema = schema; - table.Comment = comment; - - foreach (var storageParameter in storageParameters) - { - if (storageParameter.Split("=") is [var paramName, var paramValue]) - { - table[NpgsqlAnnotationNames.StorageParameterPrefix + paramName] = paramValue; - } - } - - tables.Add(table); - } - } - - GetColumns(connection, tables, filter, internalSchemas, enums, logger); - GetConstraints(connection, tables, filter, internalSchemas, out var constraintIndexes, logger); - GetIndexes(connection, tables, filter, internalSchemas, constraintIndexes, logger); - return tables; - } - - /// - /// Queries the database for defined columns and registers them with the model. - /// - private static void GetColumns( - NpgsqlConnection connection, - IReadOnlyList tables, - string? tableFilter, - string internalSchemas, - HashSet enums, - IDiagnosticsLogger logger) - { - var commandText = $""" -SELECT - nspname, - cls.relname, - typ.typname, - basetyp.typname AS basetypname, - attname, - description, - {(connection.PostgreSqlVersion >= new Version(9, 1) ? "collname" : "NULL::text as collname")}, - attisdropped, - {(connection.PostgreSqlVersion >= new Version(10, 0) ? "attidentity::text" : "' '::text as attidentity")}, - {(connection.PostgreSqlVersion >= new Version(12, 0) ? "attgenerated::text" : "' '::text as attgenerated")}, - {(connection.PostgreSqlVersion >= new Version(14, 0) ? "attcompression::text" : "''::text as attcompression")}, - format_type(typ.oid, atttypmod) AS formatted_typname, - format_type(basetyp.oid, typ.typtypmod) AS formatted_basetypname, - CASE - WHEN pg_proc.proname = 'array_recv' THEN 'a' - ELSE typ.typtype - END AS typtype, - CASE WHEN pg_proc.proname='array_recv' THEN elemtyp.typname END AS elemtypname, - NOT (attnotnull OR typ.typnotnull) AS nullable, - CASE - WHEN atthasdef THEN (SELECT pg_get_expr(adbin, cls.oid) FROM pg_attrdef WHERE adrelid = cls.oid AND adnum = attr.attnum) - END AS default, - - -- Sequence options for identity columns - {(connection.PostgreSqlVersion >= new Version(10, 0) ? - "format_type(seqtypid, 0) AS seqtype, seqstart, seqmin, seqmax, seqincrement, seqcycle, seqcache" : - "NULL AS seqtype, NULL AS seqstart, NULL AS seqmin, NULL AS seqmax, NULL AS seqincrement, NULL AS seqcycle, NULL AS seqcache")} - -FROM pg_class AS cls -JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace -LEFT JOIN pg_attribute AS attr ON attrelid = cls.oid -LEFT JOIN pg_type AS typ ON attr.atttypid = typ.oid -LEFT JOIN pg_proc ON pg_proc.oid = typ.typreceive -LEFT JOIN pg_type AS elemtyp ON (elemtyp.oid = typ.typelem) -LEFT JOIN pg_type AS basetyp ON (basetyp.oid = typ.typbasetype) -LEFT JOIN pg_description AS des ON des.objoid = cls.oid AND des.objsubid = attnum -{(connection.PostgreSqlVersion >= new Version(9, 1) ? "LEFT JOIN pg_collation as coll ON coll.oid = attr.attcollation" : "")} --- Bring in identity sequences the depend on this column -LEFT JOIN pg_depend AS dep ON dep.refobjid = cls.oid AND dep.refobjsubid = attr.attnum AND dep.deptype = 'i' -{(connection.PostgreSqlVersion >= new Version(10, 0) ? "LEFT JOIN pg_sequence AS seq ON seq.seqrelid = dep.objid" : "")} -WHERE - cls.relkind IN ('r', 'v', 'm', 'f') AND - nspname NOT IN ({internalSchemas}) AND - attnum > 0 AND - cls.relname <> '{HistoryRepository.DefaultTableName}' AND - -- Exclude tables which are members of PG extensions - NOT EXISTS ( - SELECT 1 FROM pg_depend WHERE - classid=( - SELECT cls.oid - FROM pg_class AS cls - JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace - WHERE relname='pg_class' AND ns.nspname='pg_catalog' - ) AND - objid=cls.oid AND - deptype IN ('e', 'x') - ) - {tableFilter} -ORDER BY attnum -"""; - - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - - var tableGroups = reader.Cast().GroupBy( - ddr => ( - tableSchema: ddr.GetFieldValue("nspname"), - tableName: ddr.GetFieldValue("relname"))); - - foreach (var tableGroup in tableGroups) - { - var tableSchema = tableGroup.Key.tableSchema; - var tableName = tableGroup.Key.tableName; - - var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName); - - foreach (var record in tableGroup) - { - var columnName = record.GetFieldValue("attname"); - - // We need to know about dropped columns because constraints take them into - // account when referencing columns. We'll get rid of them before returning the model. - if (record.GetValueOrDefault("attisdropped")) - { - table.Columns.Add(null!); - continue; - } - - var formattedTypeName = AdjustFormattedTypeName(record.GetFieldValue("formatted_typname")); - var formattedBaseTypeName = record.GetValueOrDefault("formatted_basetypname"); - var (storeType, systemTypeName) = formattedBaseTypeName is null - ? (formattedTypeName, record.GetFieldValue("typname")) - : (formattedBaseTypeName, record.GetFieldValue("basetypname")); // domain type - - var column = new DatabaseColumn - { - Table = table, - Name = columnName, - StoreType = storeType, - IsNullable = record.GetValueOrDefault("nullable"), - }; - - // Enum types cannot be scaffolded for now (nor can domains of enum types), - // skip with an informative message - if (enums.Contains(formattedTypeName) || formattedBaseTypeName is not null && enums.Contains(formattedBaseTypeName)) - { - logger.EnumColumnSkippedWarning($"{DisplayName(tableSchema, tableName)}.{column.Name}"); - // We need to know about skipped columns because constraints take them into - // account when referencing columns. We'll get rid of them before returning the model. - table.Columns.Add(null!); - continue; - } - - // Default values and generated columns - var defaultValueSql = record.GetValueOrDefault("default"); - switch (record.GetFieldValue("attgenerated")) - { - case "v": - column.ComputedColumnSql = defaultValueSql; - column.IsStored = false; - break; - case "s": - column.ComputedColumnSql = defaultValueSql; - column.IsStored = true; - break; - default: - column.DefaultValueSql = defaultValueSql; - column.DefaultValue = ParseDefaultValueSql(systemTypeName, defaultValueSql); - break; - } - - // Identify IDENTITY columns, as well as SERIAL ones. - var isIdentity = false; - switch (record.GetFieldValue("attidentity")) - { - case "a": - column[NpgsqlAnnotationNames.ValueGenerationStrategy] = NpgsqlValueGenerationStrategy.IdentityAlwaysColumn; - isIdentity = true; - break; - case "d": - column[NpgsqlAnnotationNames.ValueGenerationStrategy] = NpgsqlValueGenerationStrategy.IdentityByDefaultColumn; - isIdentity = true; - break; - default: - // Hacky but necessary... - // We identify serial columns by examining their default expression, and reverse-engineer these as ValueGenerated.OnAdd. - // We can't actually parse this since the table and column names are concatenated and may contain arbitrary underscores, - // so we construct various possibilities and compare against them. - // TODO: Think about composite keys? Do serial magic only for non-composite. - if (SerialTypes.Contains(systemTypeName)) - { - var seqName = $"{column.Table.Name}_{column.Name}_seq"; - if (column.Table.Schema == "public" - && (column.DefaultValueSql == $"nextval('{seqName}'::regclass)" - || column.DefaultValueSql == $"nextval('\"{seqName}\"'::regclass)") - || // non-public schema - column.DefaultValueSql == $"nextval('{column.Table.Schema}.{seqName}'::regclass)" - || column.DefaultValueSql == $"nextval('{column.Table.Schema}.\"{seqName}\"'::regclass)" - || column.DefaultValueSql == $"nextval('\"{column.Table.Schema}\".{seqName}'::regclass)" - || column.DefaultValueSql == $"nextval('\"{column.Table.Schema}\".\"{seqName}\"'::regclass)") - { - column.DefaultValueSql = null; - // Serial is the default value generation strategy, so NpgsqlAnnotationCodeGenerator - // makes sure it isn't actually rendered - column[NpgsqlAnnotationNames.ValueGenerationStrategy] = NpgsqlValueGenerationStrategy.SerialColumn; - } - } - - break; - } - - if (column[NpgsqlAnnotationNames.ValueGenerationStrategy] is not null) - { - column.ValueGenerated = ValueGenerated.OnAdd; - } - - if (isIdentity) - { - // Get the options for the associated sequence - var seqInfo = ReadSequenceInfo(record, connection.PostgreSqlVersion); - var sequenceData = new IdentitySequenceOptionsData - { - StartValue = seqInfo.StartValue, - MinValue = seqInfo.MinValue, - MaxValue = seqInfo.MaxValue, - IncrementBy = (int)(seqInfo.IncrementBy ?? 1), - IsCyclic = seqInfo.IsCyclic ?? false, - NumbersToCache = seqInfo.CacheSize ?? 1 - }; - - if (!sequenceData.Equals(IdentitySequenceOptionsData.Empty)) - { - column[NpgsqlAnnotationNames.IdentityOptions] = sequenceData.Serialize(); - } - } - - if (record.GetValueOrDefault("description") is { } comment) - { - column.Comment = comment; - } - - if (record.GetValueOrDefault("collname") is { } collation && collation != "default") - { - column.Collation = collation; - } - - if (record.GetValueOrDefault("attcompression") is { } compressionMethodChar) - { - column[NpgsqlAnnotationNames.CompressionMethod] = compressionMethodChar switch - { - "p" => "pglz", - "l" => "lz4", - _ => null - }; - } - - logger.ColumnFound( - DisplayName(tableSchema, tableName), - column.Name, - formattedTypeName, - column.IsNullable, - isIdentity, - column.DefaultValueSql, - column.ComputedColumnSql); - - table.Columns.Add(column); - } - } - } - - private static object? ParseDefaultValueSql(string systemTypeName, string? defaultValueSql) - { - defaultValueSql = defaultValueSql?.Trim(); - - if (string.IsNullOrEmpty(defaultValueSql)) - { - return null; - } - - while (defaultValueSql.StartsWith('(') && defaultValueSql.EndsWith(')')) - { - defaultValueSql = defaultValueSql[1..^1].Trim(); - } - - return systemTypeName switch - { - "bool" or "boolean" => defaultValueSql switch - { - "true" or "yes" or "on" or "1" => true, - "false" or "no" or "off" or "0" => false, - _ => null - }, - - "smallint" or "int2" => short.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @short) ? @short : null, - "integer" or "int" or "int4" => int.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @int) ? @int : null, - "bigint" or "int8" => long.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @long) ? @long : null, - - "real" or "float4" => float.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @float) ? @float : null, - "double precision" or "float8" => double.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @double) ? @double : null, - "numeric" or "decimal" => decimal.TryParse(defaultValueSql, CultureInfo.InvariantCulture, out var @decimal) ? @decimal : null, - - _ => null - }; - } - - /// - /// Queries the database for defined indexes and registers them with the model. - /// - private static void GetIndexes( - NpgsqlConnection connection, - IReadOnlyList tables, - string? tableFilter, - string internalSchemas, - List constraintIndexes, - IDiagnosticsLogger logger) - { - // Load the pg_opclass table (https://www.postgresql.org/docs/current/catalog-pg-opclass.html), - // which is referenced by the indices we'll load below - var opClasses = new Dictionary(); - try - { - using var command = new NpgsqlCommand("SELECT oid, opcname, opcdefault FROM pg_opclass", connection); - using var reader = command.ExecuteReader(); - - foreach (var opClass in reader.Cast()) - { - opClasses[opClass.GetFieldValue("oid")] = ( - opClass.GetFieldValue("opcname"), - opClass.GetFieldValue("opcdefault")); - } - } - catch (PostgresException e) - { - logger.Logger.LogWarning( - e, - "Could not load index operator classes from pg_opclass. Operator classes will not be scaffolded"); - } - - var collations = new Dictionary(); - - if (connection.PostgreSqlVersion >= new Version(9, 1)) - { - using (var command = new NpgsqlCommand("SELECT oid, collname FROM pg_collation", connection)) - using (var reader = command.ExecuteReader()) - { - foreach (var collation in reader.Cast()) - { - collations[collation.GetFieldValue("oid")] = collation.GetFieldValue("collname"); - } - } - } - - var commandText = $""" -SELECT - idxcls.oid AS idx_oid, - nspname, - cls.relname AS cls_relname, - idxcls.relname AS idx_relname, - indisunique, - {(connection.PostgreSqlVersion >= new Version(15, 0) ? "indnullsnotdistinct" : "false AS indnullsnotdistinct")}, - {(connection.PostgreSqlVersion >= new Version(11, 0) ? "indnkeyatts" : "indnatts AS indnkeyatts")}, - {(connection.PostgreSqlVersion >= new Version(9, 6) ? "pg_indexam_has_property(am.oid, 'can_order') as amcanorder" : "amcanorder")}, - indkey, - amname, - indclass, - indoption, - {(connection.PostgreSqlVersion >= new Version(9, 1) ? "indcollation" : "''::oidvector AS indcollation")}, - {(connection.PostgreSqlVersion >= new Version(8, 2) ? "idxcls.reloptions AS idx_reloptions" : "'{}'::text[] AS idx_reloptions")}, - CASE - WHEN indexprs IS NULL THEN NULL - ELSE pg_get_expr(indexprs, cls.oid) - END AS exprs, - CASE - WHEN indpred IS NULL THEN NULL - ELSE pg_get_expr(indpred, cls.oid) - END AS pred -FROM pg_class AS cls -JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace -JOIN pg_index AS idx ON indrelid = cls.oid -JOIN pg_class AS idxcls ON idxcls.oid = indexrelid -JOIN pg_am AS am ON am.oid = idxcls.relam -WHERE - cls.relkind = 'r' AND - nspname NOT IN ({internalSchemas}) AND - NOT indisprimary AND - cls.relname <> '{HistoryRepository.DefaultTableName}' AND - -- Exclude tables which are members of PG extensions - NOT EXISTS ( - SELECT 1 FROM pg_depend WHERE - classid=( - SELECT cls.oid - FROM pg_class AS cls - JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace - WHERE relname='pg_class' AND ns.nspname='pg_catalog' - ) AND - objid=cls.oid AND - deptype IN ('e', 'x') - ) - {tableFilter} -"""; - - using (var command = new NpgsqlCommand(commandText, connection)) - using (var reader = command.ExecuteReader()) - { - var tableGroups = reader.Cast().GroupBy( - ddr => ( - tableSchema: ddr.GetFieldValue("nspname"), - tableName: ddr.GetFieldValue("cls_relname"))); - - foreach (var tableGroup in tableGroups) - { - var tableSchema = tableGroup.Key.tableSchema; - var tableName = tableGroup.Key.tableName; - - var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName); - - foreach (var record in tableGroup) - { - // Constraints are detected separately (see GetConstraints), and we don't want their - // supporting indexes to appear independently. - if (constraintIndexes.Contains(record.GetFieldValue("idx_oid"))) - { - continue; - } - - var indexName = record.GetFieldValue("idx_relname"); - var index = new DatabaseIndex - { - Table = table, - Name = indexName, - IsUnique = record.GetFieldValue("indisunique") - }; - - var numKeyColumns = record.GetFieldValue("indnkeyatts"); - var columnIndices = record.GetFieldValue("indkey"); - var tableColumns = (List)table.Columns; - - if (columnIndices.Any(i => i == 0)) - { - // Expression index, not supported - logger.ExpressionIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); - continue; - - /* - var expressions = record.GetValueOrDefault("exprs"); - if (expressions is null) - throw new Exception($"Seen 0 in indkey for index {index.Name} but indexprs is null"); - index[NpgsqlAnnotationNames.IndexExpression] = expressions; - */ - } - - // Key columns come before non-key (included) columns, process them first - foreach (var i in columnIndices.Take(numKeyColumns)) - { - if (tableColumns[i - 1] is { } indexKeyColumn) - { - index.Columns.Add(indexKeyColumn); - } - else - { - logger.UnsupportedColumnIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); - goto IndexEnd; - } - } - - // Now go over non-key (included columns) if any are present - if (columnIndices.Length > numKeyColumns) - { - var nonKeyColumns = new List(); - foreach (var i in columnIndices.Skip(numKeyColumns)) - { - if (tableColumns[i - 1] is { } indexKeyColumn) - { - nonKeyColumns.Add(indexKeyColumn.Name); - } - else - { - logger.UnsupportedColumnIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); - goto IndexEnd; - } - } - - // Scaffolding included/covered properties is currently blocked, see #2194 - // index[NpgsqlAnnotationNames.IndexInclude] = nonKeyColumns.ToArray(); - } - - if (record.GetValueOrDefault("pred") is { } predicate) - { - index.Filter = predicate; - } - - // It's cleaner to always output the index method on the database model, - // even when it's btree (the default); - // NpgsqlAnnotationCodeGenerator can then omit it as by-convention. - // However, because of https://github.com/aspnet/EntityFrameworkCore/issues/11846 we omit - // the annotation from the model entirely. - if (record.GetValueOrDefault("amname") is { } indexMethod && indexMethod != "btree") - { - index[NpgsqlAnnotationNames.IndexMethod] = indexMethod; - } - - // Handle index operator classes, which we pre-loaded - var opClassNames = record - .GetFieldValue("indclass") - .Select(oid => opClasses.TryGetValue(oid, out var opc) && !opc.IsDefault ? opc.Name : null) - .ToArray(); - - if (opClassNames.Any(op => op is not null)) - { - index[NpgsqlAnnotationNames.IndexOperators] = opClassNames; - } - - var columnCollations = record - .GetFieldValue("indcollation") - .Select(oid => collations.TryGetValue(oid, out var collation) && collation != "default" ? collation : null) - .ToArray(); - - if (columnCollations.Any(coll => coll is not null)) - { - index[RelationalAnnotationNames.Collation] = columnCollations; - } - - if (record.GetValueOrDefault("amcanorder")) - { - var options = record.GetFieldValue("indoption"); - - // The first bit in indoption specifies whether values are sorted in descending order, the second whether - // NULLs are sorted first instead of last. - var isDescending = options.Select(val => (val & 0x0001) != 0).ToList(); - var nullSortOrders = options - .Select(val => (val & 0x0002) != 0 ? NullSortOrder.NullsFirst : NullSortOrder.NullsLast) - .ToArray(); - - index.IsDescending = isDescending; - - if (!SortOrderHelper.IsDefaultNullSortOrder(nullSortOrders, isDescending)) - { - index[NpgsqlAnnotationNames.IndexNullSortOrder] = nullSortOrders; - } - } - - if (record.GetValueOrDefault("indnullsnotdistinct")) - { - index[NpgsqlAnnotationNames.NullsDistinct] = false; - } - - foreach (var storageParameter in record.GetValueOrDefault("idx_reloptions") ?? []) - { - if (storageParameter.Split("=") is [var paramName, var paramValue]) - { - index[NpgsqlAnnotationNames.StorageParameterPrefix + paramName] = paramValue; - } - } - - table.Indexes.Add(index); - - IndexEnd: ; - } - } - } - } - - /// - /// Queries the database for defined constraints and registers them with the model. - /// - private static void GetConstraints( - NpgsqlConnection connection, - IReadOnlyList tables, - string? tableFilter, - string internalSchemas, - out List constraintIndexes, - IDiagnosticsLogger logger) - { - var commandText = $""" -SELECT - ns.nspname, - cls.relname, - conname, - contype::text, - conkey, - conindid, - frnns.nspname AS fr_nspname, - frncls.relname AS fr_relname, - confkey, - confdeltype::text -FROM pg_class AS cls -JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace -JOIN pg_constraint as con ON con.conrelid = cls.oid -LEFT OUTER JOIN pg_class AS frncls ON frncls.oid = con.confrelid -LEFT OUTER JOIN pg_namespace as frnns ON frnns.oid = frncls.relnamespace -WHERE - cls.relkind = 'r' AND - ns.nspname NOT IN ({internalSchemas}) AND - con.contype IN ('p', 'f', 'u') AND - cls.relname <> '{HistoryRepository.DefaultTableName}' AND - -- Exclude tables which are members of PG extensions - NOT EXISTS ( - SELECT 1 FROM pg_depend WHERE - classid=( - SELECT cls.oid - FROM pg_class AS cls - JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace - WHERE relname='pg_class' AND ns.nspname='pg_catalog' - ) AND - objid=cls.oid AND - deptype IN ('e', 'x') - ) - {tableFilter} -"""; - - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - - constraintIndexes = []; - var tableGroups = reader.Cast().GroupBy( - ddr => ( - tableSchema: ddr.GetFieldValue("nspname"), - tableName: ddr.GetFieldValue("relname"))); - - foreach (var tableGroup in tableGroups) - { - var tableSchema = tableGroup.Key.tableSchema; - var tableName = tableGroup.Key.tableName; - - var table = tables.Single(t => t.Schema == tableSchema && t.Name == tableName); - - // Primary keys - foreach (var primaryKeyRecord in tableGroup.Where(ddr => ddr.GetFieldValue("contype") == "p")) - { - var pkName = primaryKeyRecord.GetValueOrDefault("conname"); - var primaryKey = new DatabasePrimaryKey { Table = table, Name = pkName }; - - foreach (var pkColumnIndex in primaryKeyRecord.GetFieldValue("conkey")) - { - if (table.Columns[pkColumnIndex - 1] is { } pkColumn) - { - primaryKey.Columns.Add(pkColumn); - } - else - { - logger.UnsupportedColumnConstraintSkippedWarning(primaryKey.Name, DisplayName(tableSchema, tableName)); - goto PkEnd; - } - } - - table.PrimaryKey = primaryKey; - PkEnd: ; - } - - // Foreign keys - foreach (var foreignKeyRecord in tableGroup.Where(ddr => ddr.GetFieldValue("contype") == "f")) - { - var fkName = foreignKeyRecord.GetFieldValue("conname"); - var principalTableSchema = foreignKeyRecord.GetFieldValue("fr_nspname"); - var principalTableName = foreignKeyRecord.GetFieldValue("fr_relname"); - var onDeleteAction = foreignKeyRecord.GetFieldValue("confdeltype"); - - var principalTable = - tables.FirstOrDefault( - t => - principalTableSchema == t.Schema && principalTableName == t.Name) - ?? tables.FirstOrDefault( - t => - principalTableSchema.Equals(t.Schema, StringComparison.OrdinalIgnoreCase) - && principalTableName.Equals(t.Name, StringComparison.OrdinalIgnoreCase)); - - if (principalTable is null) - { - logger.ForeignKeyReferencesMissingPrincipalTableWarning( - fkName, - DisplayName(table.Schema, table.Name), - DisplayName(principalTableSchema, principalTableName)); - - continue; - } - - var foreignKey = new DatabaseForeignKey - { - Table = table, - Name = fkName, - PrincipalTable = principalTable, - OnDelete = ConvertToReferentialAction(onDeleteAction) - }; - - var columnIndices = foreignKeyRecord.GetFieldValue("conkey"); - var principalColumnIndices = foreignKeyRecord.GetFieldValue("confkey"); - - if (columnIndices.Length != principalColumnIndices.Length) - { - throw new InvalidOperationException("Found varying lengths for column and principal column indices."); - } - - var principalColumns = (List)principalTable.Columns; - - for (var i = 0; i < columnIndices.Length; i++) - { - var foreignKeyColumn = table.Columns[columnIndices[i] - 1]; - var foreignKeyPrincipalColumn = principalColumns[principalColumnIndices[i] - 1]; - if (foreignKeyColumn is null || foreignKeyPrincipalColumn is null) - { - logger.UnsupportedColumnConstraintSkippedWarning(foreignKey.Name, DisplayName(tableSchema, tableName)); - goto ForeignKeyEnd; - } - - foreignKey.Columns.Add(foreignKeyColumn); - foreignKey.PrincipalColumns.Add(foreignKeyPrincipalColumn); - } - - table.ForeignKeys.Add(foreignKey); - ForeignKeyEnd: ; - } - - // Unique constraints - foreach (var record in tableGroup.Where(ddr => ddr.GetValueOrDefault("contype") == "u")) - { - var name = record.GetValueOrDefault("conname"); - - logger.UniqueConstraintFound(name, DisplayName(tableSchema, tableName)); - - var uniqueConstraint = new DatabaseUniqueConstraint { Table = table, Name = name }; - - foreach (var columnIndex in record.GetFieldValue("conkey")) - { - var constraintColumn = table.Columns[columnIndex - 1]; - if (constraintColumn is null) - { - logger.UnsupportedColumnConstraintSkippedWarning(uniqueConstraint.Name, DisplayName(tableSchema, tableName)); - goto UniqueConstraintEnd; - } - - uniqueConstraint.Columns.Add(constraintColumn); - } - - table.UniqueConstraints.Add(uniqueConstraint); - constraintIndexes.Add(record.GetValueOrDefault("conindid")); - - UniqueConstraintEnd: ; - } - } - } - - /// - /// Queries the database for defined sequences and registers them with the model. - /// - private static IEnumerable GetSequences( - NpgsqlConnection connection, - DatabaseModel databaseModel, - Func? schemaFilter, - IDiagnosticsLogger logger) - { - // 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 typname, - {(connection.PostgreSqlVersion >= new Version(9, 1) ? "start_value" : "1")}::bigint AS seqstart, - minimum_value::bigint AS seqmin, - maximum_value::bigint AS seqmax, - increment::bigint AS seqincrement, - 1::bigint AS seqcache, - CASE - WHEN cycle_option = 'YES' THEN TRUE - ELSE FALSE - END AS seqcycle -FROM information_schema.sequences -JOIN pg_namespace AS ns ON ns.nspname = sequence_schema -JOIN pg_class AS cls ON cls.relnamespace = ns.oid AND cls.relname = sequence_name -WHERE - cls.relkind = 'S' - /* AND seqtype IN ('integer', 'bigint', 'smallint') */ - /* Filter out owned serial and identity sequences */ - AND 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 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; - } - } - } - - /// - /// Queries the database for defined enums and registers them with the model. - /// - private static HashSet GetEnums(NpgsqlConnection connection, DatabaseModel databaseModel) - { - var enums = new HashSet(); - - // pg_enum doesn't exist on Redshift - if (connection.PostgreSqlVersion < new Version(8, 3)) - { - return enums; - } - - var commandText = $""" -SELECT - nspname, - typname, - array_agg(enumlabel{(connection.PostgreSqlVersion >= new Version(9, 1) ? " ORDER BY enumsortorder" : "")}) AS labels -FROM pg_enum -JOIN pg_type ON pg_type.oid = enumtypid -JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace -GROUP BY nspname, typname -"""; - - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - - // TODO: just return a collection and make this a static utility method. - while (reader.Read()) - { - var schema = reader.GetFieldValue("nspname"); - var name = reader.GetFieldValue("typname"); - var labels = reader.GetFieldValue("labels"); - - if (schema == "public") - { - schema = null; - } - - PostgresEnum.GetOrAddPostgresEnum(databaseModel, schema, name, labels); - enums.Add(name); - } - - return enums; - } - - /// - /// Queries the installed database extensions and registers them with the model. - /// - private static void GetExtensions(NpgsqlConnection connection, DatabaseModel databaseModel) - { - const string commandText = """ -SELECT ns.nspname, extname, extversion -FROM pg_extension -JOIN pg_namespace ns ON ns.oid=extnamespace -"""; - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - - while (reader.Read()) - { - var schema = reader.GetFieldValue("nspname"); - var name = reader.GetString(reader.GetOrdinal("extname")); - var version = reader.GetValueOrDefault("extversion"); - - if (name == "plpgsql") // Implicitly installed in all PG databases - { - continue; - } - - databaseModel.GetOrAddPostgresExtension(schema, name, version); - } - } - - private static void GetCollations( - NpgsqlConnection connection, - DatabaseModel databaseModel, - string internalSchemas, - IDiagnosticsLogger logger) - { - var commandText = $""" -SELECT - nspname, collname, collprovider, collcollate, collctype, - {(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 -WHERE - nspname NOT IN ({internalSchemas}) -"""; - - try - { - using var command = new NpgsqlCommand(commandText, connection); - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - var schema = reader.GetString(reader.GetOrdinal("nspname")); - var name = reader.GetString(reader.GetOrdinal("collname")); - var icuLocale = reader.GetValueOrDefault("colllocale"); - var lcCollate = reader.GetValueOrDefault("collcollate"); - var lcCtype = reader.GetValueOrDefault("collctype"); - var providerCode = reader.GetChar(reader.GetOrdinal("collprovider")); - var isDeterministic = reader.GetBoolean(reader.GetOrdinal("collisdeterministic")); - - string? provider; - switch (providerCode) - { - case 'c': - provider = "libc"; - break; - case 'i': - provider = "icu"; - break; - case 'd': - provider = null; - break; - default: - logger.Logger.LogWarning( - $"Unknown collation provider code {providerCode} for collation {name}, skipping."); - continue; - } - - // Starting with PG15, ICU collations only have colliculocale populated. - if (lcCollate is null || lcCtype is null) - { - Debug.Assert(lcCollate is null && lcCtype is null); - Debug.Assert(icuLocale is not null); - lcCollate = icuLocale; - lcCtype = icuLocale; - } - - logger.CollationFound(schema, name, lcCollate, lcCtype, provider, isDeterministic); - - PostgresCollation.GetOrAddCollation( - databaseModel, schema, name, lcCollate!, lcCtype, provider, isDeterministic); - } - } - catch (PostgresException e) - { - logger.Logger.LogWarning(e, "Could not load database collations."); - } - } - - #endregion - - #region SequenceInfo - - private static SequenceInfo ReadSequenceInfo(DbDataRecord record, Version postgresVersion) - { - 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 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: - defaultMin = 1; - defaultMax = short.MaxValue; - defaultStart = minValue; - break; - - case "smallint": - // PostgreSQL 10 changed the default minvalue for a descending sequence, see #264 - defaultMin = postgresVersion >= new Version(10, 0) - ? short.MinValue - : short.MinValue + 1; - defaultMax = -1; - defaultStart = maxValue; - break; - - case "integer" when incrementBy > 0: - defaultMin = 1; - defaultMax = int.MaxValue; - defaultStart = minValue; - break; - - case "integer": - // PostgreSQL 10 changed the default minvalue for a descending sequence, see #264 - defaultMin = postgresVersion >= new Version(10, 0) - ? int.MinValue - : int.MinValue + 1; - defaultMax = -1; - defaultStart = maxValue; - break; - - case "bigint" when incrementBy > 0: - defaultMin = 1; - defaultMax = long.MaxValue; - defaultStart = minValue; - break; - - case "bigint": - // PostgreSQL 10 changed the default minvalue for a descending sequence, see #264 - defaultMin = postgresVersion >= new Version(10, 0) - ? long.MinValue - : long.MinValue + 1; - defaultMax = -1; - defaultStart = maxValue; - break; - - default: - throw new NotSupportedException($"Sequence has datatype {storeType} which isn't an expected sequence type."); - } - - return new SequenceInfo(storeType) - { - StartValue = startValue == defaultStart ? null : startValue, - MinValue = minValue == defaultMin ? null : minValue, - MaxValue = maxValue == defaultMax ? null : maxValue, - IncrementBy = incrementBy == 1 ? null : incrementBy, - IsCyclic = isCyclic == false ? null : true, - CacheSize = cacheSize is 1 or null ? null : cacheSize - }; - } - - private sealed class SequenceInfo(string storeType) - { - 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 int? CacheSize { get; set; } - } - - #endregion - - #region Filter fragment generators - - /// - /// Builds a delegate to generate a schema filter fragment. - /// - private static Func? GenerateSchemaFilter(IReadOnlyList schemas) - => schemas.Any() - ? s => $"{s} IN ({string.Join(", ", schemas.Select(EscapeLiteral))})" - : null; - - /// - /// Builds a delegate to generate a table filter fragment. - /// - private static Func? GenerateTableFilter( - IReadOnlyList<(string? Schema, string Table)> tables, - Func? schemaFilter) - => schemaFilter is not null || tables.Any() - ? (s, t) => - { - var tableFilterBuilder = new StringBuilder(); - - var openBracket = false; - if (schemaFilter is not null) - { - tableFilterBuilder - .Append("(") - .Append(schemaFilter(s)); - openBracket = true; - } - - if (tables.Any()) - { - if (openBracket) - { - tableFilterBuilder - .AppendLine() - .Append("OR "); - } - else - { - tableFilterBuilder.Append("("); - openBracket = true; - } - - var tablesWithoutSchema = tables.Where(e => string.IsNullOrEmpty(e.Schema)).ToList(); - if (tablesWithoutSchema.Any()) - { - tableFilterBuilder.Append(t); - tableFilterBuilder.Append(" IN ("); - tableFilterBuilder.Append(string.Join(", ", tablesWithoutSchema.Select(e => EscapeLiteral(e.Table)))); - tableFilterBuilder.Append(")"); - } - - var tablesWithSchema = tables.Where(e => !string.IsNullOrEmpty(e.Schema)).ToList(); - if (tablesWithSchema.Any()) - { - if (tablesWithoutSchema.Any()) - { - tableFilterBuilder.Append(" OR "); - } - - tableFilterBuilder.Append(t); - tableFilterBuilder.Append(" IN ("); - tableFilterBuilder.Append(string.Join(", ", tablesWithSchema.Select(e => EscapeLiteral(e.Table)))); - tableFilterBuilder.Append(") AND ("); - tableFilterBuilder.Append(s); - tableFilterBuilder.Append(" || '.' || "); - tableFilterBuilder.Append(t); - tableFilterBuilder.Append(") IN ("); - tableFilterBuilder.Append(string.Join(", ", tablesWithSchema.Select(e => EscapeLiteral($"{e.Schema}.{e.Table}")))); - tableFilterBuilder.Append(")"); - } - } - - if (openBracket) - { - tableFilterBuilder.Append(")"); - } - - return tableFilterBuilder.ToString(); - } - : null; - - #endregion - - #region Utilities - - /// - /// Type names as returned by PostgreSQL's format_type need to be cleaned up a bit - /// - private static string AdjustFormattedTypeName(string formattedTypeName) - { - // User-defined types (e.g. enums) with capital letters get formatted with quotes, remove. - if (formattedTypeName[0] == '"') - { - formattedTypeName = formattedTypeName.Substring(1, formattedTypeName.Length - 2); - } - - if (formattedTypeName == "bpchar") - { - formattedTypeName = "char"; - } - - return formattedTypeName; - } - - /// - /// Maps a character to a . - /// - private static ReferentialAction ConvertToReferentialAction(string onDeleteAction) - => onDeleteAction switch - { - "a" => ReferentialAction.NoAction, - "r" => ReferentialAction.Restrict, - "c" => ReferentialAction.Cascade, - "n" => ReferentialAction.SetNull, - "d" => ReferentialAction.SetDefault, - _ => throw new ArgumentOutOfRangeException( - $"Unknown value {onDeleteAction} for foreign key deletion action code.") - }; - - /// - /// Constructs the display name given a schema and table name. - /// - // TODO: should this default to/screen out the public schema? - private static string DisplayName(string? schema, string name) - => string.IsNullOrEmpty(schema) ? name : $"{schema}.{name}"; - - /// - /// Parses the table name into a tuple of schema name and table name where the schema may be null. - /// - private static (string? Schema, string Table) Parse(string table) - { - var match = SchemaTableNameExtractor.Match(table.Trim()); - - if (!match.Success) - { - throw new InvalidOperationException("The table name could not be parsed."); - } - - var part1 = match.Groups["part1"].Value; - var part2 = match.Groups["part2"].Value; - - return string.IsNullOrEmpty(part2) ? (null, part1) : (part1, part2); - } - - /// - /// Wraps a string literal in single quotes. - /// - private static string EscapeLiteral(string? s) - => $"'{s}'"; - - #endregion -} diff --git a/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs deleted file mode 100644 index f8f62958f9..0000000000 --- a/src/EFCore.PG/Storage/Internal/INpgsqlRelationalConnection.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 -/// 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 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 - /// 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. - /// - INpgsqlRelationalConnection CreateAdminConnection(); - - /// - /// 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. - /// - ValueTask CloneWith(string connectionString, bool async, CancellationToken cancellationToken = default); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/INpgsqlTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/INpgsqlTypeMapping.cs deleted file mode 100644 index 645ae31cd7..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/INpgsqlTypeMapping.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 interface INpgsqlTypeMapping -{ - /// - /// The database type used by Npgsql. - /// - NpgsqlDbType NpgsqlDbType { get; } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs deleted file mode 100644 index 4bae5b1402..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System.Collections; -using System.Data; -using System.Data.Common; -using System.Text; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; -using Microsoft.EntityFrameworkCore.Storage.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.ValueConversion; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// Type mapping for PostgreSQL arrays. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/arrays.html -/// -public abstract class NpgsqlArrayTypeMapping : RelationalTypeMapping -{ - /// - /// 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 NpgsqlArrayTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping ElementTypeMapping - { - get - { - var elementTypeMapping = base.ElementTypeMapping; - Check.DebugAssert( - elementTypeMapping is not null, - "NpgsqlArrayTypeMapping without an element type mapping"); - Check.DebugAssert( - elementTypeMapping is RelationalTypeMapping, - "NpgsqlArrayTypeMapping with a non-relational element type mapping"); - return (RelationalTypeMapping)elementTypeMapping; - } - } -} - -/// -/// Type mapping for PostgreSQL arrays. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/arrays.html -/// -public class NpgsqlArrayTypeMapping : NpgsqlArrayTypeMapping -{ - /// - /// 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 NpgsqlArrayTypeMapping Default { get; } - = new(); - - /// - /// The database type used by Npgsql. - /// - public virtual NpgsqlDbType? NpgsqlDbType { 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. - /// - [UsedImplicitly] - public NpgsqlArrayTypeMapping(RelationalTypeMapping elementTypeMapping) - : this(elementTypeMapping.StoreType + "[]", elementTypeMapping) - { - } - - /// - /// 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. - /// - [UsedImplicitly] - public NpgsqlArrayTypeMapping(string storeType, RelationalTypeMapping elementTypeMapping) - : this(CreateParameters(storeType, elementTypeMapping)) - { - Check.DebugAssert(storeType.EndsWith("[]", StringComparison.Ordinal), "NpgsqlArrayTypeMapping created for a non-array store type"); - } - - private static RelationalTypeMappingParameters CreateParameters(string storeType, RelationalTypeMapping elementMapping) - { - ValueConverter? converter = null; - - // We do GetElementType for multidimensional arrays - these don't implement generic IEnumerable<> - var elementType = typeof(TCollection).TryGetElementType(typeof(IEnumerable<>)) ?? typeof(TCollection).GetElementType(); - - Check.DebugAssert(elementType is not null, "modelElementType cannot be null"); - - if (elementMapping.Converter is { } elementConverter) - { - var providerElementType = elementConverter.ProviderClrType; - - // Nullability has been unwrapped on the element converter's provider CLR type, so add it back here if needed - if (elementType.IsNullableValueType()) - { - providerElementType = providerElementType.MakeNullable(); - } - - converter = (ValueConverter)Activator.CreateInstance( - typeof(NpgsqlArrayConverter<,,>).MakeGenericType( - typeof(TCollection), typeof(TConcreteCollection), providerElementType.MakeArrayType()), - elementConverter)!; - } - else if (typeof(TCollection) != typeof(TConcreteCollection)) - { - converter = (ValueConverter)Activator.CreateInstance( - typeof(NpgsqlArrayConverter<,,>).MakeGenericType( - typeof(TCollection), typeof(TConcreteCollection), elementType.MakeArrayType()))!; - } - -#pragma warning disable EF1001 - var comparer = typeof(TCollection).IsArray && typeof(TCollection).GetArrayRank() > 1 - ? null // TODO: Value comparer for multidimensional arrays - : (ValueComparer?)Activator.CreateInstance( - elementType.IsNullableValueType() || elementMapping.Comparer.Type.IsNullableValueType() - ? 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 && !typeof(TElement).UnwrapNullableType().IsAssignableTo(elementJsonReaderWriter.ValueType)) - { - throw new InvalidOperationException( - $"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 - // NpgsqlRowValueTypeMapping). - // TODO: Also, we don't (yet) support JSON serialization of multidimensional arrays. - var collectionJsonReaderWriter = - elementJsonReaderWriter is null || typeof(TCollection).IsArray && typeof(TCollection).GetArrayRank() > 1 - ? null - : (JsonValueReaderWriter?)Activator.CreateInstance( - (elementType.IsNullableValueType() - ? typeof(JsonCollectionOfNullableStructsReaderWriter<,>) - : elementType.IsValueType - ? typeof(JsonCollectionOfStructsReaderWriter<,>) - : typeof(JsonCollectionOfReferencesReaderWriter<,>)) - .MakeGenericType(typeof(TConcreteCollection), elementType.UnwrapNullableType()), - elementJsonReaderWriter); - - return new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - typeof(TCollection), converter, comparer, elementMapping: elementMapping, - jsonValueReaderWriter: collectionJsonReaderWriter), - storeType); - } - - /// - /// 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 NpgsqlArrayTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - var clrType = parameters.CoreParameters.ClrType; - - if (clrType.TryGetElementType(typeof(IEnumerable<>)) == null && clrType.GetElementType() == null) - { - throw new ArgumentException($"CLR type '{parameters.CoreParameters.ClrType}' isn't an IEnumerable"); - } - - // If the element mapping has an NpgsqlDbType or DbType, set our own NpgsqlDbType as an array of that. - // 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 { NpgsqlDbType: not NpgsqlTypes.NpgsqlDbType.Unknown } elementNpgsqlTypeMapping - ? elementNpgsqlTypeMapping.NpgsqlDbType - : ElementTypeMapping.DbType.HasValue - ? new NpgsqlParameter { DbType = ElementTypeMapping.DbType.Value }.NpgsqlDbType - : default(NpgsqlDbType?)); - } - - // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled - // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. - private NpgsqlArrayTypeMapping() - : base(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(TCollection), elementMapping: NullMapping), - "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 override DbParameter CreateParameter( - DbCommand command, - string name, - object? value, - bool? nullable = null, - ParameterDirection direction = ParameterDirection.Input) - { - // 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 && Converter is null && !value.GetType().IsArrayOrGenericList()) - { - switch (value) - { - case IEnumerable elements: - value = elements.ToList(); - break; - - case IEnumerable elements: - value = elements.Cast().ToList(); - break; - } - } - - 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; - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - { - Check.DebugAssert( - parameters.CoreParameters.ClrType == typeof(TCollection), "NpgsqlArrayTypeMapping.Clone attempting to change ClrType"); - Check.DebugAssert( - parameters.CoreParameters.ElementTypeMapping is not null, "NpgsqlArrayTypeMapping.Clone without an element type mapping"); - Check.DebugAssert( - parameters.CoreParameters.ElementTypeMapping.ClrType == typeof(TElement).UnwrapNullableType(), - "NpgsqlArrayTypeMapping.Clone attempting to change element ClrType"); - - return new NpgsqlArrayTypeMapping(parameters); - } - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - if (value is not IEnumerable enumerable) - { - throw new ArgumentException($"'{value.GetType().Name}' must be an IEnumerable", nameof(value)); - } - - if (value is Array array && array.Rank != 1) - { - throw new NotSupportedException("Multidimensional array literals aren't supported"); - } - - var sb = new StringBuilder(); - sb.Append("ARRAY["); - - var isFirst = true; - foreach (var element in enumerable) - { - if (isFirst) - { - isFirst = false; - } - else - { - sb.Append(","); - } - - sb.Append(ElementTypeMapping.GenerateProviderValueSqlLiteral(element)); - } - - sb.Append("]::"); - sb.Append(ElementTypeMapping.StoreType); - sb.Append("[]"); - return sb.ToString(); - } - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - { - if (parameter is not NpgsqlParameter npgsqlParameter) - { - throw new ArgumentException( - $"Npgsql-specific type mapping {GetType()} being used with non-Npgsql parameter type {parameter.GetType().Name}"); - } - - if (NpgsqlDbType.HasValue) - { - npgsqlParameter.NpgsqlDbType = NpgsqlDbType.Value; - } - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBigIntegerTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBigIntegerTypeMapping.cs deleted file mode 100644 index 4be7eb2f0e..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBigIntegerTypeMapping.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Numerics; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlBigIntegerTypeMapping : 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 NpgsqlBigIntegerTypeMapping Default { 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 NpgsqlBigIntegerTypeMapping() - : base("numeric", typeof(BigInteger), NpgsqlDbType.Numeric, jsonValueReaderWriter: JsonBigIntegerReaderWriter.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. - /// - protected NpgsqlBigIntegerTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Numeric) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlBigIntegerTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null - ? storeType - : parameters.Scale is null - ? $"numeric({parameters.Precision})" - : $"numeric({parameters.Precision},{parameters.Scale})"; - - /// - /// 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 deleted file mode 100644 index 977fbd56ec..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBitTypeMapping.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections; -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL bit string type. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-bit.html -/// -public class NpgsqlBitTypeMapping : 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 NpgsqlBitTypeMapping Default { get; } = new(); - - /// - /// Constructs an instance of the class. - /// - public NpgsqlBitTypeMapping() - : base("bit", typeof(BitArray), NpgsqlDbType.Bit, jsonValueReaderWriter: JsonBitArrayReaderWriter.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. - /// - protected NpgsqlBitTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Bit) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlBitTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var bits = (BitArray)value; - var sb = new StringBuilder(); - sb.Append("B'"); - for (var i = 0; i < bits.Count; i++) - { - sb.Append(bits[i] ? '1' : '0'); - } - - sb.Append('\''); - return sb.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var bits = (BitArray)value; - var exprs = new Expression[bits.Count]; - for (var i = 0; i < bits.Count; i++) - { - exprs[i] = Expression.Constant(bits[i]); - } - - return Expression.New(Constructor, Expression.NewArrayInit(typeof(bool), exprs)); - } - - private static readonly ConstructorInfo Constructor = - typeof(BitArray).GetConstructor([typeof(bool[])])!; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBoolTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBoolTypeMapping.cs deleted file mode 100644 index 26c139cfe2..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlBoolTypeMapping.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlBoolTypeMapping : BoolTypeMapping -{ - /// - /// 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 new NpgsqlBoolTypeMapping Default { 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 NpgsqlBoolTypeMapping() - : base("boolean") - { - } - - /// - /// 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 NpgsqlBoolTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlBoolTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => (bool)value ? "TRUE" : "FALSE"; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlByteArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlByteArrayTypeMapping.cs deleted file mode 100644 index 8e1074b3be..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlByteArrayTypeMapping.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Globalization; -using System.Text; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlByteArrayTypeMapping : RelationalTypeMapping -{ - /// - /// 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 NpgsqlByteArrayTypeMapping Default { 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 NpgsqlByteArrayTypeMapping() - : base("bytea", typeof(byte[]), System.Data.DbType.Binary, jsonValueReaderWriter: JsonByteArrayReaderWriter.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. - /// - protected NpgsqlByteArrayTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlByteArrayTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - Check.NotNull(value, nameof(value)); - var bytea = (byte[])value; - - var builder = new StringBuilder(bytea.Length * 2 + 6); - - builder.Append("BYTEA E'\\\\x"); - foreach (var b in bytea) - { - builder.Append(b.ToString("X2", CultureInfo.InvariantCulture)); - } - - builder.Append('\''); - - return builder.ToString(); - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterCharMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterCharMapping.cs deleted file mode 100644 index c38a8a9f00..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterCharMapping.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Data.Common; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// Type mapping for the PostgreSQL 'character' data type. Handles both CLR strings and chars. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-character.html -/// -public class NpgsqlCharacterCharTypeMapping : CharTypeMapping, INpgsqlTypeMapping -{ - /// - /// 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 new NpgsqlCharacterCharTypeMapping Default { get; } = new("text"); - - /// - public virtual NpgsqlDbType NpgsqlDbType - => NpgsqlDbType.Char; - - /// - /// 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 NpgsqlCharacterCharTypeMapping(string storeType) - : this( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(char), jsonValueReaderWriter: JsonCharReaderWriter.Instance), - storeType, - StoreTypePostfix.Size, - System.Data.DbType.StringFixedLength, - unicode: false, - fixedLength: 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 NpgsqlCharacterCharTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlCharacterCharTypeMapping(parameters); - - /// - /// 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 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}"); - } - - base.ConfigureParameter(parameter); - npgsqlParameter.NpgsqlDbType = NpgsqlDbType; - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs deleted file mode 100644 index 5df6a72198..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCharacterStringTypeMapping.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Data.Common; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// Type mapping for the PostgreSQL 'character' data type. Handles both CLR strings and chars. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-character.html -/// -/// -public class NpgsqlCharacterStringTypeMapping : NpgsqlStringTypeMapping -{ - /// - /// 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 new NpgsqlCharacterStringTypeMapping Default { get; } = new("text"); - - /// - /// Static for fixed-width character types. - /// - /// - ///

- /// Comparisons of 'character' data as defined in the SQL standard differ dramatically from CLR string - /// comparisons. This value comparer adjusts for this by only comparing strings after truncating trailing - /// whitespace. - ///

- ///

- /// Note that if a value converter is used and the CLR type isn't a string at all, we just use the default - /// value converter instead. - ///

- ///
- private static readonly ValueComparer CharacterValueComparer = - new( - (x, y) => EqualsWithoutTrailingWhitespace(x, y), - x => GetHashCodeWithoutTrailingWhitespace(x)); - - /// - /// 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 ValueComparer Comparer - => ClrType == typeof(string) ? CharacterValueComparer : base.Comparer; - - /// - /// 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 ValueComparer KeyComparer - => Comparer; - - /// - /// 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 NpgsqlCharacterStringTypeMapping(string storeType, int size = 1) - : this( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(string), jsonValueReaderWriter: JsonStringReaderWriter.Instance), - storeType, - StoreTypePostfix.Size, - System.Data.DbType.StringFixedLength, - unicode: false, - size, - fixedLength: 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 NpgsqlCharacterStringTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Char) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlCharacterStringTypeMapping( - new RelationalTypeMappingParameters( - parameters.CoreParameters, - parameters.StoreType, - StoreTypePostfix.Size, - parameters.DbType, - parameters.Unicode, - parameters.Size, - parameters.FixedLength, - parameters.Precision, - parameters.Scale)); - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - { - if (parameter.Value is string value) - { - parameter.Value = value.TrimEnd(); - } - - base.ConfigureParameter(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. - /// - public static bool EqualsWithoutTrailingWhitespace(string? a, string? b) - => (a, b) switch - { - (null, null) => true, - (_, null) => false, - (null, _) => false, - _ => a.AsSpan().TrimEnd().SequenceEqual(b.AsSpan().TrimEnd()) - }; - - /// - /// 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/NpgsqlCidrLegacyTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrLegacyTypeMapping.cs deleted file mode 100644 index 9e2f797439..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrLegacyTypeMapping.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Net; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - -/// -/// The type mapping for the PostgreSQL cidr type. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-CIDR -/// -public class NpgsqlLegacyCidrTypeMapping : 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 NpgsqlLegacyCidrTypeMapping Default { 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 NpgsqlLegacyCidrTypeMapping() - : base("cidr", typeof(NpgsqlCidr), NpgsqlDbType.Cidr, JsonCidrLegacyReaderWriter.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. - /// - protected NpgsqlLegacyCidrTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Cidr) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlLegacyCidrTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var cidr = (NpgsqlCidr)value; - return $"CIDR '{cidr.Address}/{cidr.Netmask}'"; - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var cidr = (NpgsqlCidr)value; - return Expression.New( - NpgsqlCidrConstructor, - Expression.Call(ParseMethod, Expression.Constant(cidr.Address.ToString())), - Expression.Constant(cidr.Netmask)); - } - - private static readonly MethodInfo ParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; - - private static readonly ConstructorInfo NpgsqlCidrConstructor = - typeof(NpgsqlCidr).GetConstructor([typeof(IPAddress), typeof(byte)])!; - - /// - /// 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 JsonCidrLegacyReaderWriter : JsonValueReaderWriter - { - private static readonly PropertyInfo InstanceProperty = typeof(JsonCidrLegacyReaderWriter).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 JsonCidrLegacyReaderWriter 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/NpgsqlCidrTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrTypeMapping.cs deleted file mode 100644 index 86c4c6ef20..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCidrTypeMapping.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Net; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL cidr type. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-CIDR -/// -public class NpgsqlCidrTypeMapping : 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 NpgsqlCidrTypeMapping Default { 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 NpgsqlCidrTypeMapping() - : base("cidr", typeof(IPNetwork), NpgsqlDbType.Cidr, JsonCidrReaderWriter.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. - /// - protected NpgsqlCidrTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Cidr) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlCidrTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var ipNetwork = (IPNetwork)value; - return $"CIDR '{ipNetwork.BaseAddress}/{ipNetwork.PrefixLength}'"; - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var cidr = (IPNetwork)value; - return Expression.New( - NpgsqlCidrConstructor, - Expression.Call(ParseMethod, Expression.Constant(cidr.BaseAddress.ToString())), - Expression.Constant(cidr.PrefixLength)); - } - - private static readonly MethodInfo ParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; - - private static readonly ConstructorInfo NpgsqlCidrConstructor = - typeof(IPNetwork).GetConstructor([typeof(IPAddress), 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 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 IPNetwork FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) - => IPNetwork.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, IPNetwork ipNetwork) - => writer.WriteStringValue(ipNetwork.ToString()); - - /// - public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs deleted file mode 100644 index 1b1d6ea481..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCubeTypeMapping.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Globalization; -using System.Text; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlCubeTypeMapping : 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 NpgsqlCubeTypeMapping Default { 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 NpgsqlCubeTypeMapping() - : base("cube", typeof(NpgsqlCube), NpgsqlDbType.Cube) - { - } - - /// - /// 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 NpgsqlCubeTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Cube) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlCubeTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"'{(NpgsqlCube)value}'::cube"; - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var cube = (NpgsqlCube)value; - - var lowerLeftArray = Expression.NewArrayInit( - typeof(double), - cube.LowerLeft.Select(coord => Expression.Constant(coord))); - - if (cube.IsPoint) - { - return Expression.New(PointConstructor, lowerLeftArray); - } - - var upperRightArray = Expression.NewArrayInit( - typeof(double), - cube.UpperRight.Select(coord => Expression.Constant(coord))); - - return Expression.New(Constructor, lowerLeftArray, upperRightArray); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlCube).GetConstructor([typeof(IEnumerable), typeof(IEnumerable)])!; - - private static readonly ConstructorInfo PointConstructor = - typeof(NpgsqlCube).GetConstructor([typeof(IEnumerable)])!; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs deleted file mode 100644 index 3b6be8c3b9..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlDateOnlyTypeMapping : 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 NpgsqlDateOnlyTypeMapping Default { 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 NpgsqlDateOnlyTypeMapping() - : base("date", typeof(DateOnly), NpgsqlDbType.Date, NpgsqlJsonDateOnlyReaderWriter.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. - /// - protected NpgsqlDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Date) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlDateOnlyTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"DATE '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => Format((DateOnly)value); - - private static string Format(DateOnly date) - { - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (date == DateOnly.MinValue) - { - return "-infinity"; - } - - if (date == DateOnly.MaxValue) - { - return "infinity"; - } - } - - return date.ToString("yyyy-MM-dd", 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 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()!; - - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - switch (s) - { - case "-infinity": - return DateOnly.MinValue; - case "infinity": - return DateOnly.MaxValue; - } - } - - 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 deleted file mode 100644 index c966b483ba..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlDateTimeDateTypeMapping : 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 NpgsqlDateTimeDateTypeMapping Default { 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 NpgsqlDateTimeDateTypeMapping() - : base("date", typeof(DateTime), NpgsqlDbType.Date, NpgsqlJsonDateTimeReaderWriter.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. - /// - protected NpgsqlDateTimeDateTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Date) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlDateTimeDateTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"DATE '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => Format((DateTime)value); - - private static string Format(DateTime date) - { - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (date == DateTime.MinValue) - { - return "-infinity"; - } - - if (date == DateTime.MaxValue) - { - return "infinity"; - } - } - - return date.ToString("yyyy-MM-dd", 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 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()!; - - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - switch (s) - { - case "-infinity": - return DateTime.MinValue; - case "infinity": - return DateTime.MaxValue; - } - } - - 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/NpgsqlDecimalTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDecimalTypeMapping.cs deleted file mode 100644 index 3b6849b1bd..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDecimalTypeMapping.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlDecimalTypeMapping : NpgsqlTypeMapping -{ - private const string DecimalFormatConst = "{0:0.0###########################}"; - - /// - /// 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 NpgsqlDecimalTypeMapping Default { 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 NpgsqlDecimalTypeMapping(Type? clrType = null) - : this( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - clrType ?? typeof(decimal), - jsonValueReaderWriter: clrType == typeof(decimal) || clrType is null - ? JsonDecimalReaderWriter.Instance - : clrType == typeof(double) - ? JsonDoubleReaderWriter.Instance - : clrType == typeof(float) - ? JsonFloatReaderWriter.Instance - : throw new ArgumentException("clrType must be decimal, double or float", nameof(clrType)) - ), - "numeric")) - { - } - - /// - /// 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 NpgsqlDecimalTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Numeric) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlDecimalTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null - ? storeType - : parameters.Scale is null - ? $"numeric({parameters.Precision})" - : $"numeric({parameters.Precision},{parameters.Scale})"; - - /// - /// 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 SqlLiteralFormatString - => DecimalFormatConst; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDoubleTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDoubleTypeMapping.cs deleted file mode 100644 index b74797dd08..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDoubleTypeMapping.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlDoubleTypeMapping : DoubleTypeMapping -{ - /// - /// 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 new NpgsqlDoubleTypeMapping Default { 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 NpgsqlDoubleTypeMapping() - : base("double precision", System.Data.DbType.Double) - { - } - - /// - /// 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 NpgsqlDoubleTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlDoubleTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => Convert.ToDouble(value) switch - { - var d when double.IsNaN(d) => "'NaN'", - var d when double.IsPositiveInfinity(d) => "'Infinity'", - var d when double.IsNegativeInfinity(d) => "'-Infinity'", - var d => base.GenerateNonNullSqlLiteral(d) - }; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEStringTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEStringTypeMapping.cs deleted file mode 100644 index f6d485909b..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEStringTypeMapping.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// Represents a so-called PostgreSQL E-string literal string, which allows C-style escape sequences. -/// This is a "virtual" type mapping which is never returned by . -/// It is only used internally by some method translators to produce literal strings. -/// -/// -/// See https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS -/// -public class NpgsqlEStringTypeMapping : StringTypeMapping -{ - /// - /// 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 new static NpgsqlEStringTypeMapping Default { 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 NpgsqlEStringTypeMapping() - : base("does_not_exist", System.Data.DbType.String) - { - } - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"E'{EscapeSqlLiteral((string)value)}'"; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs deleted file mode 100644 index 108892bb33..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Data.Common; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlEnumTypeMapping : RelationalTypeMapping -{ - /// - /// 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 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 virtual string UnquotedStoreType { 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 static NpgsqlEnumTypeMapping Default { 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 NpgsqlEnumTypeMapping( - string quotedStoreType, - string unquotedStoreType, - Type enumType, - IReadOnlyDictionary labels) - : base( - quotedStoreType, - enumType, - jsonValueReaderWriter: (JsonValueReaderWriter?)Activator.CreateInstance( - typeof(JsonPgEnumReaderWriter<>).MakeGenericType(enumType))) - { - if (!enumType.IsEnum || !enumType.IsValueType) - { - throw new ArgumentException($"Enum type mappings require a CLR enum. {enumType.FullName} is not an enum."); - } - - UnquotedStoreType = unquotedStoreType; - Labels = 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. - /// - protected NpgsqlEnumTypeMapping( - RelationalTypeMappingParameters parameters, - string unquotedStoreType, - IReadOnlyDictionary labels) - : base(parameters) - { - UnquotedStoreType = unquotedStoreType; - Labels = labels; - } - - // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled - // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. - private NpgsqlEnumTypeMapping() - : base("some_enum", typeof(int)) - { - UnquotedStoreType = "some_enum"; - Labels = new Dictionary(); - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => 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 - /// 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 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 - /// 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 GenerateNonNullSqlLiteral(object value) - => $"'{Labels[value]}'::{StoreType}"; - - // This is public for 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 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 - /// 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 T FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) - => Enum.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, T value) - => writer.WriteStringValue(value.ToString()); - - /// - public override Expression ConstructorExpression => Expression.Property(null, InstanceProperty); - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlFloatTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlFloatTypeMapping.cs deleted file mode 100644 index a689c5c80b..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlFloatTypeMapping.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlFloatTypeMapping : FloatTypeMapping -{ - /// - /// 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 new NpgsqlFloatTypeMapping Default { 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 NpgsqlFloatTypeMapping() - : base("real", System.Data.DbType.Single) - { - } - - /// - /// 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 NpgsqlFloatTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlFloatTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var singleValue = Convert.ToSingle(value); - if (double.IsNaN(singleValue)) - { - return "'NaN'"; - } - - if (double.IsPositiveInfinity(singleValue)) - { - return "'Infinity'"; - } - - if (double.IsNegativeInfinity(singleValue)) - { - return "'-Infinity'"; - } - - return base.GenerateNonNullSqlLiteral(singleValue); - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs deleted file mode 100644 index d934514295..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlGeometricTypeMapping.cs +++ /dev/null @@ -1,597 +0,0 @@ -using System.Globalization; -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlPointTypeMapping : 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 NpgsqlPointTypeMapping Default { 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 NpgsqlPointTypeMapping() - : base("point", typeof(NpgsqlPoint), NpgsqlDbType.Point) - { - } - - /// - /// 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 NpgsqlPointTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Point) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlPointTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var point = (NpgsqlPoint)value; - return FormattableString.Invariant($"POINT '({point.X:G17},{point.Y:G17})'"); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var point = (NpgsqlPoint)value; - return Expression.New(Constructor, Expression.Constant(point.X), Expression.Constant(point.Y)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlPoint).GetConstructor([typeof(double), typeof(double)])!; -} - -/// -/// 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 NpgsqlLineTypeMapping : 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 NpgsqlLineTypeMapping Default { 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 NpgsqlLineTypeMapping() - : base("line", typeof(NpgsqlLine), NpgsqlDbType.Line) - { - } - - /// - /// 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 NpgsqlLineTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Line) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlLineTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var line = (NpgsqlLine)value; - var a = line.A.ToString("G17", CultureInfo.InvariantCulture); - var b = line.B.ToString("G17", CultureInfo.InvariantCulture); - var c = line.C.ToString("G17", CultureInfo.InvariantCulture); - return $"LINE '{{{a},{b},{c}}}'"; - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var line = (NpgsqlLine)value; - return Expression.New( - Constructor, - Expression.Constant(line.A), Expression.Constant(line.B), Expression.Constant(line.C)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlLine).GetConstructor([typeof(double), typeof(double), typeof(double)])!; -} - -/// -/// 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 NpgsqlLineSegmentTypeMapping : 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 NpgsqlLineSegmentTypeMapping Default { 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 NpgsqlLineSegmentTypeMapping() - : base("lseg", typeof(NpgsqlLSeg), NpgsqlDbType.LSeg) - { - } - - /// - /// 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 NpgsqlLineSegmentTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.LSeg) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlLineSegmentTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var lseg = (NpgsqlLSeg)value; - return FormattableString.Invariant($"LSEG '[({lseg.Start.X:G17},{lseg.Start.Y:G17}),({lseg.End.X:G17},{lseg.End.Y:G17})]'"); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var lseg = (NpgsqlLSeg)value; - return Expression.New( - Constructor, - Expression.Constant(lseg.Start.X), Expression.Constant(lseg.Start.Y), - Expression.Constant(lseg.End.X), Expression.Constant(lseg.End.Y)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlLSeg).GetConstructor([typeof(double), typeof(double), typeof(double), typeof(double)])!; -} - -/// -/// 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 NpgsqlBoxTypeMapping : 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 NpgsqlBoxTypeMapping Default { 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 NpgsqlBoxTypeMapping() - : base("box", typeof(NpgsqlBox), NpgsqlDbType.Box) - { - } - - /// - /// 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 NpgsqlBoxTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Box) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlBoxTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var box = (NpgsqlBox)value; - return FormattableString.Invariant($"BOX '(({box.Right:G17},{box.Top:G17}),({box.Left:G17},{box.Bottom:G17}))'"); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var box = (NpgsqlBox)value; - return Expression.New( - Constructor, - Expression.Constant(box.Top), Expression.Constant(box.Right), - Expression.Constant(box.Bottom), Expression.Constant(box.Left)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlBox).GetConstructor([typeof(double), typeof(double), typeof(double), typeof(double)])!; -} - -/// -/// 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 NpgsqlPathTypeMapping : 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 NpgsqlPathTypeMapping Default { 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 NpgsqlPathTypeMapping() - : base("path", typeof(NpgsqlPath), NpgsqlDbType.Path) - { - } - - /// - /// 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 NpgsqlPathTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Path) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlPathTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var path = (NpgsqlPath)value; - var sb = new StringBuilder(); - sb.Append("PATH '"); - sb.Append(path.Open ? '[' : '('); - for (var i = 0; i < path.Count; i++) - { - sb.Append('('); - sb.Append(path[i].X.ToString("G17", CultureInfo.InvariantCulture)); - sb.Append(','); - sb.Append(path[i].Y.ToString("G17", CultureInfo.InvariantCulture)); - sb.Append(')'); - if (i < path.Count - 1) - { - sb.Append(','); - } - } - - sb.Append(path.Open ? ']' : ')'); - sb.Append('\''); - return sb.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var path = (NpgsqlPath)value; - return Expression.New( - Constructor, - Expression.NewArrayInit( - typeof(NpgsqlPoint), - path.Select( - p => Expression.New( - PointConstructor, - Expression.Constant(p.X), Expression.Constant(p.Y)))), - Expression.Constant(path.Open)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlPath).GetConstructor([typeof(IEnumerable), typeof(bool)])!; - - private static readonly ConstructorInfo PointConstructor = - typeof(NpgsqlPoint).GetConstructor([typeof(double), typeof(double)])!; -} - -/// -/// 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 NpgsqlPolygonTypeMapping : 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 NpgsqlPolygonTypeMapping Default { 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 NpgsqlPolygonTypeMapping() - : base("polygon", typeof(NpgsqlPolygon), NpgsqlDbType.Polygon) - { - } - - /// - /// 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 NpgsqlPolygonTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Polygon) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlPolygonTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var polygon = (NpgsqlPolygon)value; - var sb = new StringBuilder(); - sb.Append("POLYGON '("); - for (var i = 0; i < polygon.Count; i++) - { - sb.Append('('); - sb.Append(polygon[i].X.ToString("G17", CultureInfo.InvariantCulture)); - sb.Append(','); - sb.Append(polygon[i].Y.ToString("G17", CultureInfo.InvariantCulture)); - sb.Append(')'); - if (i < polygon.Count - 1) - { - sb.Append(','); - } - } - - sb.Append(")'"); - return sb.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var polygon = (NpgsqlPolygon)value; - return Expression.New( - Constructor, - Expression.NewArrayInit( - typeof(NpgsqlPoint), - polygon.Select( - p => Expression.New( - PointConstructor, - Expression.Constant(p.X), Expression.Constant(p.Y))))); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlPolygon).GetConstructor([typeof(NpgsqlPoint[])])!; - - private static readonly ConstructorInfo PointConstructor = - typeof(NpgsqlPoint).GetConstructor([typeof(double), typeof(double)])!; -} - -/// -/// 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 NpgsqlCircleTypeMapping : 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 NpgsqlCircleTypeMapping Default { 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 NpgsqlCircleTypeMapping() - : base("circle", typeof(NpgsqlCircle), NpgsqlDbType.Circle) - { - } - - /// - /// 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 NpgsqlCircleTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Circle) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlCircleTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var circle = (NpgsqlCircle)value; - return FormattableString.Invariant($"CIRCLE '<({circle.X:G17},{circle.Y:G17}),{circle.Radius:G17}>'"); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var circle = (NpgsqlCircle)value; - return Expression.New( - Constructor, - Expression.Constant(circle.X), Expression.Constant(circle.Y), Expression.Constant(circle.Radius)); - } - - private static readonly ConstructorInfo Constructor = - 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 deleted file mode 100644 index 3d6eef2f0e..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Collections.Immutable; -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL hstore type. Supports both -/// and over strings. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/hstore.html -/// -public class NpgsqlHstoreTypeMapping : NpgsqlTypeMapping -{ - private static readonly HstoreMutableComparer MutableComparerInstance = 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 static NpgsqlHstoreTypeMapping Default { get; } = new(typeof(Dictionary)); - - /// - /// 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 NpgsqlHstoreTypeMapping(Type clrType) - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType, comparer: GetComparer(clrType)), - "hstore"), - NpgsqlDbType.Hstore) - { - } - - /// - /// 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 NpgsqlHstoreTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Hstore) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlHstoreTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var sb = new StringBuilder("HSTORE '"); - foreach (var kv in (IReadOnlyDictionary)value) - { - sb.Append('"'); - sb.Append(kv.Key); // TODO: Escape - sb.Append("\"=>"); - if (kv.Value is null) - { - sb.Append("NULL"); - } - else - { - sb.Append('"'); - sb.Append(kv.Value); // TODO: Escape - sb.Append("\","); - } - } - - sb.Remove(sb.Length - 1, 1); - - sb.Append('\''); - return sb.ToString(); - } - - private static ValueComparer? GetComparer(Type clrType) - { - if (clrType == typeof(Dictionary)) - { - return MutableComparerInstance; - } - - if (clrType == typeof(ImmutableDictionary)) - { - // Because ImmutableDictionary is immutable, we can use the default value comparer, which doesn't - // clone for snapshot and just does reference comparison. - // We could compare contents here if the references are different, but that would penalize the 99% case - // where a different reference means different contents, which would only save a very rare database update. - return null; - } - - throw new ArgumentException( - $"CLR type must be {nameof(Dictionary)} or {nameof(ImmutableDictionary)}"); - } - - private sealed class HstoreMutableComparer() : ValueComparer>( - (a, b) => Compare(a, b), - o => o.GetHashCode(), - o => new Dictionary(o)) - { - private static bool Compare(Dictionary? a, Dictionary? b) - { - if (a is null) - { - return b is null; - } - - if (b is null || a.Count != b.Count) - { - return false; - } - - foreach (var kv in a) - { - if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs deleted file mode 100644 index ad99bd3c63..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlInetTypeMapping.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Net; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL inet type. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-INET -/// -public class NpgsqlInetTypeMapping : 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 NpgsqlInetTypeMapping Default { get; } = new(typeof(IPAddress)); - - /// - /// 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 NpgsqlInetTypeMapping(Type clrType) - : base( - "inet", - clrType, - NpgsqlDbType.Inet, - jsonValueReaderWriter: clrType == typeof(IPAddress) - ? JsonIPAddressReaderWriter.Instance - : clrType == typeof(NpgsqlInet) - ? JsonNpgsqlInetReaderWriter.Instance - : throw new ArgumentException($"Only {nameof(IPAddress)} and {nameof(NpgsqlInet)} are supported", nameof(clrType))) - { - } - - /// - /// 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 NpgsqlInetTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Inet) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlInetTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"INET '{value}'"; - - /// - /// 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 Expression GenerateCodeLiteral(object value) - => value switch - { - IPAddress ip => Expression.Call(IPAddressParseMethod, Expression.Constant(ip.ToString())), - NpgsqlInet ip => Expression.New(NpgsqlInetConstructor, Expression.Constant(ip.ToString())), - _ => throw new UnreachableException() - }; - - private static readonly MethodInfo IPAddressParseMethod = typeof(IPAddress).GetMethod("Parse", [typeof(string)])!; - private static readonly ConstructorInfo NpgsqlInetConstructor = typeof(NpgsqlInet).GetConstructor([typeof(string)])!; - - /// - /// 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); - } - - /// - /// 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 deleted file mode 100644 index 7bf603944b..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlIntervalTypeMapping : 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 NpgsqlIntervalTypeMapping Default { 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 NpgsqlIntervalTypeMapping() - : this( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(TimeSpan), jsonValueReaderWriter: NpgsqlJsonTimeSpanReaderWriter.Instance), - "interval")) - { - } - - /// - /// 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 NpgsqlIntervalTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Interval) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlIntervalTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null ? storeType : $"interval({parameters.Precision})"; - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"INTERVAL '{FormatTimeSpanAsInterval((TimeSpan)value)}'"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => $""" - "{FormatTimeSpanAsInterval((TimeSpan)value)}" - """; - - /// - /// 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 FormatTimeSpanAsInterval(TimeSpan ts) - => ts.ToString( - $@"{(ts < TimeSpan.Zero ? "\\-" : "")}{(ts.Days == 0 ? "" : "d\\ ")}hh\:mm\:ss{(ts.Ticks % 10000000 == 0 ? "" : "\\.FFFFFF")}", - 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 static TimeSpan ParseIntervalAsTimeSpan(ReadOnlySpan s) - { - // We store the a PG-compatible interval representation in JSON so it can be queried out, but unfortunately this isn't - // compatible with TimeSpan, so we have to parse manually. - - // Note - var isNegated = false; - if (s[0] == '-') - { - isNegated = true; - s = s[1..]; - } - - int i; - for (i = 0; i < s.Length; i++) - { - if (!char.IsDigit(s[i])) - { - break; - } - } - - if (i == s.Length) - { - throw new FormatException(); - } - - // Space is the separator between the days and the hours:minutes:seconds components - var days = 0; - if (s[i] == ' ') - { - days = int.Parse(s[..i]); - s = s[i..]; - } - - var timeSpan = TimeSpan.Parse(s, CultureInfo.InvariantCulture); - - // We've already extracted the days, so there shouldn't be a days component in the rest (indicates an incompatible/malformed string) - if (timeSpan.Days != 0) - { - throw new FormatException(); - } - - if (days > 0) - { - timeSpan = new TimeSpan( - days, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds, timeSpan.Microseconds); - } - - if (isNegated) - { - timeSpan = -timeSpan; - } - - return timeSpan; - } - - /// - /// 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 deleted file mode 100644 index 151e088fc8..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlJsonTypeMapping.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Text; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// Represents scalars within a JSON document that maintain their json/jsonb type (rather than being extracted out as e.g. text/int). -/// Also supports the older Npgsql-specific JSON mapping, allowing mapping json/jsonb to text, to e.g. -/// (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled). -/// Note that for structural types mapped via the standard EF complex/owned mapping, we use -/// . -/// -public class NpgsqlJsonTypeMapping : 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 NpgsqlJsonTypeMapping Default { get; } = new("jsonb", typeof(JsonElement)); - - /// - /// 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 NpgsqlJsonTypeMapping(string storeType, Type clrType, CoreTypeMapping? elementTypeMapping = null) - : base( - storeType, - clrType, - storeType == "jsonb" ? NpgsqlDbType.Jsonb : NpgsqlDbType.Json, - jsonValueReaderWriter: JsonStringReaderWriter.Instance, - elementTypeMapping: elementTypeMapping) - { - if (storeType != "json" && storeType != "jsonb") - { - throw new ArgumentException($"{nameof(storeType)} must be 'json' or 'jsonb'", nameof(storeType)); - } - } - - /// - /// 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 NpgsqlJsonTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters, npgsqlDbType) - { - } - - /// - /// 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 bool IsJsonb - => StoreType == "jsonb"; - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlJsonTypeMapping(parameters, NpgsqlDbType); - - /// - /// 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 string EscapeSqlLiteral(string literal) - => Check.NotNull(literal, nameof(literal)).Replace("'", "''"); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - switch (value) - { - case JsonDocument _: - case JsonElement _: - { - using var stream = new MemoryStream(); - using var writer = new Utf8JsonWriter(stream); - if (value is JsonDocument doc) - { - doc.WriteTo(writer); - } - else - { - ((JsonElement)value).WriteTo(writer); - } - - writer.Flush(); - return $"'{EscapeSqlLiteral(Encoding.UTF8.GetString(stream.ToArray()))}'"; - } - case string s: - return $"'{EscapeSqlLiteral(s)}'"; - default: // User POCO - return $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'"; - } - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - => value switch - { - JsonDocument document => Expression.Call( - ParseMethod, Expression.Constant(document.RootElement.ToString()), DefaultJsonDocumentOptions), - JsonElement element => Expression.Property( - Expression.Call(ParseMethod, Expression.Constant(element.ToString()), DefaultJsonDocumentOptions), - nameof(JsonDocument.RootElement)), - string s => Expression.Constant(s), - _ => throw new NotSupportedException("Cannot generate code literals for JSON POCOs") - }; - - private static readonly Expression DefaultJsonDocumentOptions = Expression.New(typeof(JsonDocumentOptions)); - - private static readonly MethodInfo ParseMethod = - 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 deleted file mode 100644 index 8f31c436f3..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlLTreeTypeMapping.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlLTreeTypeMapping : NpgsqlStringTypeMapping -{ - 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 - /// 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 new NpgsqlLTreeTypeMapping Default { 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 NpgsqlLTreeTypeMapping() - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - typeof(LTree), - new ValueConverter(l => l, s => new LTree(s)), - jsonValueReaderWriter: JsonLTreeReaderWriter.Instance), - "ltree"), - NpgsqlDbType.LTree) - { - } - - /// - /// 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 NpgsqlLTreeTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.LTree) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlLTreeTypeMapping(parameters); - - /// - /// 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 Expression GenerateCodeLiteral(object value) - => Expression.New(Constructor, Expression.Constant((string)(LTree)value, typeof(string))); - - /// - /// 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 deleted file mode 100644 index c73b206421..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddr8TypeMapping.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Net.NetworkInformation; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL macaddr8 type. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-MACADDR8 -/// -public class NpgsqlMacaddr8TypeMapping : 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 NpgsqlMacaddr8TypeMapping Default { 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 NpgsqlMacaddr8TypeMapping() - : base( - "macaddr8", - typeof(PhysicalAddress), - NpgsqlDbType.MacAddr8, - jsonValueReaderWriter: JsonMacaddrReaderWriter.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. - /// - protected NpgsqlMacaddr8TypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.MacAddr8) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlMacaddr8TypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"MACADDR8 '{(PhysicalAddress)value}'"; - - /// - /// 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 Expression GenerateCodeLiteral(object value) - => Expression.Call(ParseMethod, Expression.Constant(((PhysicalAddress)value).ToString())); - - 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 deleted file mode 100644 index 25ccc25bdd..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMacaddrTypeMapping.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Net.NetworkInformation; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL macaddr type. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/datatype-net-types.html#DATATYPE-MACADDR -/// -public class NpgsqlMacaddrTypeMapping : 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 NpgsqlMacaddrTypeMapping Default { 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 NpgsqlMacaddrTypeMapping() - : base("macaddr", typeof(PhysicalAddress), NpgsqlDbType.MacAddr, jsonValueReaderWriter: JsonMacaddrReaderWriter.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. - /// - protected NpgsqlMacaddrTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.MacAddr) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlMacaddrTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"MACADDR '{(PhysicalAddress)value}'"; - - /// - /// 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 Expression GenerateCodeLiteral(object value) - => Expression.Call(ParseMethod, Expression.Constant(((PhysicalAddress)value).ToString())); - - private static readonly MethodInfo ParseMethod = typeof(PhysicalAddress).GetMethod("Parse", [typeof(string)])!; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMoneyTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMoneyTypeMapping.cs deleted file mode 100644 index b8d10537ff..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMoneyTypeMapping.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlMoneyTypeMapping : DecimalTypeMapping -{ - /// - /// 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 new NpgsqlMoneyTypeMapping Default { 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 NpgsqlMoneyTypeMapping() - : base("money", System.Data.DbType.Currency) - { - } - - /// - /// 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 NpgsqlMoneyTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlMoneyTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => base.GenerateNonNullSqlLiteral(value) + "::money"; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs deleted file mode 100644 index bffde21f38..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Collections; -using System.Data.Common; -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for PostgreSQL multirange types. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/rangetypes.html -/// -public class NpgsqlMultirangeTypeMapping : RelationalTypeMapping -{ - /// - /// The relational type mapping of the ranges contained in this multirange. - /// - public virtual NpgsqlRangeTypeMapping RangeMapping - => (NpgsqlRangeTypeMapping)ElementTypeMapping!; - - /// - /// The relational type mapping of the values contained in this multirange. - /// - public virtual RelationalTypeMapping SubtypeMapping { get; } - - /// - /// The database type used by Npgsql. - /// - public virtual NpgsqlDbType NpgsqlDbType { get; } - - /// - /// Constructs an instance of the class. - /// - /// The database type to map - /// The CLR type to map. - /// The type mapping of the ranges contained in this multirange. - public NpgsqlMultirangeTypeMapping(string storeType, Type clrType, NpgsqlRangeTypeMapping rangeMapping) - // TODO: Need to do comparer, converter - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType, elementMapping: rangeMapping), - storeType)) - { - SubtypeMapping = rangeMapping.SubtypeMapping; - NpgsqlDbType = GenerateNpgsqlDbType(rangeMapping.SubtypeMapping); - } - - /// - /// 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 NpgsqlMultirangeTypeMapping( - RelationalTypeMappingParameters parameters, - NpgsqlDbType npgsqlDbType) - : base(parameters) - { - var rangeMapping = (NpgsqlRangeTypeMapping)parameters.CoreParameters.ElementTypeMapping!; - - SubtypeMapping = rangeMapping.SubtypeMapping; - NpgsqlDbType = npgsqlDbType; - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlMultirangeTypeMapping(parameters, NpgsqlDbType); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => GenerateNonNullSqlLiteral(value, RangeMapping, StoreType); - - /// - /// 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 GenerateNonNullSqlLiteral(object value, RelationalTypeMapping rangeMapping, string multirangeStoreType) - { - var multirange = (IList)value; - - var sb = new StringBuilder(); - sb.Append("'{"); - - for (var i = 0; i < multirange.Count; i++) - { - sb.Append(rangeMapping.GenerateEmbeddedSqlLiteral(multirange[i])); - if (i < multirange.Count - 1) - { - sb.Append(", "); - } - } - - sb.Append("}'::"); - sb.Append(multirangeStoreType); - return sb.ToString(); - } - - private static NpgsqlDbType GenerateNpgsqlDbType(RelationalTypeMapping subtypeMapping) - { - NpgsqlDbType subtypeNpgsqlDbType; - if (subtypeMapping is INpgsqlTypeMapping npgsqlTypeMapping) - { - subtypeNpgsqlDbType = npgsqlTypeMapping.NpgsqlDbType; - } - else - { - // We're using a built-in, non-Npgsql mapping such as IntTypeMapping. - // Infer the NpgsqlDbType from the DbType (somewhat hacky but why not). - Debug.Assert(subtypeMapping.DbType.HasValue); - var p = new NpgsqlParameter { DbType = subtypeMapping.DbType.Value }; - subtypeNpgsqlDbType = p.NpgsqlDbType; - } - - return NpgsqlDbType.Multirange | subtypeNpgsqlDbType; - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - // Note that arrays are handled in EF Core's CSharpHelper, so this method doesn't get called for them. - - // Unfortunately, List> requires MemberInit, which CSharpHelper doesn't support - var type = value.GetType(); - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) - { - throw new NotSupportedException("Cannot generate code literals for List, consider using arrays instead"); - } - - throw new InvalidCastException(); - } - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - { - if (parameter is not NpgsqlParameter npgsqlParameter) - { - throw new ArgumentException( - $"Npgsql-specific type mapping {GetType()} being used with non-Npgsql parameter type {parameter.GetType().Name}"); - } - - npgsqlParameter.NpgsqlDbType = NpgsqlDbType; - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs deleted file mode 100644 index f107520cbb..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlPgLsnTypeMapping.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Text; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for the PostgreSQL pg_lsn type. -/// -/// -/// See: https://www.postgresql.org/docs/current/datatype-pg-lsn.html -/// -public class NpgsqlPgLsnTypeMapping : 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 NpgsqlPgLsnTypeMapping Default { 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 NpgsqlPgLsnTypeMapping() - : base( - "pg_lsn", typeof(NpgsqlLogSequenceNumber), NpgsqlDbType.PgLsn, - jsonValueReaderWriter: JsonLogSequenceNumberReaderWriter.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. - /// - protected NpgsqlPgLsnTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.PgLsn) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlPgLsnTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var lsn = (NpgsqlLogSequenceNumber)value; - var builder = new StringBuilder("PG_LSN '") - .Append(lsn.ToString()) - .Append('\''); - return builder.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var lsn = (NpgsqlLogSequenceNumber)value; - return Expression.New(Constructor, Expression.Constant((ulong)lsn)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlLogSequenceNumber).GetConstructor([typeof(ulong)])!; - - /// - /// 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 deleted file mode 100644 index e738b8170e..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The type mapping for PostgreSQL range types. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/rangetypes.html -/// -public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping -{ - private PropertyInfo? _isEmptyProperty; - private PropertyInfo? _lowerProperty; - private PropertyInfo? _upperProperty; - private PropertyInfo? _lowerInclusiveProperty; - private PropertyInfo? _upperInclusiveProperty; - private PropertyInfo? _lowerInfiniteProperty; - private PropertyInfo? _upperInfiniteProperty; - - private ConstructorInfo? _rangeConstructor1; - private ConstructorInfo? _rangeConstructor2; - private ConstructorInfo? _rangeConstructor3; - - /// - /// 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 NpgsqlRangeTypeMapping Default { get; } = new(); - - // ReSharper disable once MemberCanBePrivate.Global - /// - /// The relational type mapping of the range's subtype. - /// - public virtual RelationalTypeMapping SubtypeMapping { get; } - - /// - /// For user-defined ranges, we have no and so the PG type name is set on - /// instead. - /// - public virtual string? UnquotedStoreType { get; init; } - - /// - /// Constructs an instance of the class for a built-in range type which has a - /// defined. - /// - /// The database type to map - /// The CLR type to map. - /// The of the built-in range. - /// The type mapping for the range subtype. - public static NpgsqlRangeTypeMapping CreatBuiltInRangeMapping( - string rangeStoreType, - Type rangeClrType, - NpgsqlDbType rangeNpgsqlDbType, - RelationalTypeMapping subtypeMapping) - => new(rangeStoreType, rangeClrType, rangeNpgsqlDbType, subtypeMapping); - - /// - /// Constructs an instance of the class for a user-defined range type which doesn't have a - /// defined. - /// - /// The database type to map, quoted. - /// The database type to map, unquoted. - /// The CLR type to map. - /// The type mapping for the range subtype. - public static NpgsqlRangeTypeMapping CreatUserDefinedRangeMapping( - string quotedRangeStoreType, - string unquotedRangeStoreType, - Type rangeClrType, - RelationalTypeMapping subtypeMapping) - => new(quotedRangeStoreType, rangeClrType, rangeNpgsqlDbType: NpgsqlDbType.Unknown, subtypeMapping) - { - UnquotedStoreType = unquotedRangeStoreType - }; - - private NpgsqlRangeTypeMapping( - string rangeStoreType, - Type rangeClrType, - NpgsqlDbType rangeNpgsqlDbType, - RelationalTypeMapping subtypeMapping) - : base(rangeStoreType, rangeClrType, rangeNpgsqlDbType) - { - SubtypeMapping = subtypeMapping; - } - - /// - /// 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 NpgsqlRangeTypeMapping( - RelationalTypeMappingParameters parameters, - NpgsqlDbType npgsqlDbType, - RelationalTypeMapping subtypeMapping) - : base(parameters, npgsqlDbType) - { - SubtypeMapping = subtypeMapping; - } - - // This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled - // models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details. - private NpgsqlRangeTypeMapping() - : this("int4range", typeof(NpgsqlRange), NpgsqlDbType.IntegerRange, subtypeMapping: null!) - { - } - - /// - /// 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 NpgsqlRangeTypeMapping Clone(NpgsqlDbType npgsqlDbType, RelationalTypeMapping subtypeTypeMapping) - => new(Parameters, npgsqlDbType, subtypeTypeMapping); - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlRangeTypeMapping(parameters, NpgsqlDbType, SubtypeMapping); - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - { - // Built-in range types have an NpgsqlDbType, so we just do the normal thing. - if (UnquotedStoreType is null) - { - Check.DebugAssert(NpgsqlDbType is not NpgsqlDbType.Unknown, "NpgsqlDbType is Unknown but no PgDataTypeName is configured"); - base.ConfigureParameter(parameter); - return; - } - - Check.DebugAssert(NpgsqlDbType is NpgsqlDbType.Unknown, "PgDataTypeName is non-null, but NpgsqlDbType is " + NpgsqlDbType); - - 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 - /// 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 GenerateNonNullSqlLiteral(object value) - => $"'{GenerateEmbeddedNonNullSqlLiteral(value)}'::{StoreType}"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - { - InitializeAccessors(ClrType, SubtypeMapping.ClrType); - - var builder = new StringBuilder(); - - if ((bool)_isEmptyProperty.GetValue(value)!) - { - builder.Append("empty"); - } - else - { - builder.Append((bool)_lowerInclusiveProperty.GetValue(value)! ? '[' : '('); - - if (!(bool)_lowerInfiniteProperty.GetValue(value)!) - { - builder.Append(SubtypeMapping.GenerateEmbeddedSqlLiteral(_lowerProperty.GetValue(value))); - } - - builder.Append(','); - - if (!(bool)_upperInfiniteProperty.GetValue(value)!) - { - builder.Append(SubtypeMapping.GenerateEmbeddedSqlLiteral(_upperProperty.GetValue(value))); - } - - builder.Append((bool)_upperInclusiveProperty.GetValue(value)! ? ']' : ')'); - } - - return builder.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - InitializeAccessors(ClrType, SubtypeMapping.ClrType); - - var lower = _lowerProperty.GetValue(value); - var upper = _upperProperty.GetValue(value); - var lowerInclusive = (bool)_lowerInclusiveProperty.GetValue(value)!; - var upperInclusive = (bool)_upperInclusiveProperty.GetValue(value)!; - var lowerInfinite = (bool)_lowerInfiniteProperty.GetValue(value)!; - var upperInfinite = (bool)_upperInfiniteProperty.GetValue(value)!; - - return lowerInfinite || upperInfinite - ? Expression.New( - _rangeConstructor3, - Expression.Constant(lower), - Expression.Constant(lowerInclusive), - Expression.Constant(lowerInfinite), - Expression.Constant(upper), - Expression.Constant(upperInclusive), - Expression.Constant(upperInfinite)) - : lowerInclusive && upperInclusive - ? Expression.New( - _rangeConstructor1, - Expression.Constant(lower), - Expression.Constant(upper)) - : Expression.New( - _rangeConstructor2, - Expression.Constant(lower), - Expression.Constant(lowerInclusive), - Expression.Constant(upper), - Expression.Constant(upperInclusive)); - } - - [MemberNotNull( - "_isEmptyProperty", - "_lowerProperty", "_upperProperty", - "_lowerInclusiveProperty", "_upperInclusiveProperty", - "_lowerInfiniteProperty", "_upperInfiniteProperty", - "_rangeConstructor1", "_rangeConstructor2", "_rangeConstructor3")] - private void InitializeAccessors(Type rangeClrType, Type subtypeClrType) - { - _isEmptyProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.IsEmpty))!; - _lowerProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.LowerBound))!; - _upperProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.UpperBound))!; - _lowerInclusiveProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.LowerBoundIsInclusive))!; - _upperInclusiveProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.UpperBoundIsInclusive))!; - _lowerInfiniteProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.LowerBoundInfinite))!; - _upperInfiniteProperty = rangeClrType.GetProperty(nameof(NpgsqlRange.UpperBoundInfinite))!; - - _rangeConstructor1 = rangeClrType.GetConstructor( - [subtypeClrType, subtypeClrType])!; - _rangeConstructor2 = rangeClrType.GetConstructor( - [subtypeClrType, typeof(bool), subtypeClrType, typeof(bool)])!; - _rangeConstructor3 = rangeClrType.GetConstructor( - [subtypeClrType, typeof(bool), typeof(bool), subtypeClrType, typeof(bool), typeof(bool)])!; - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRegconfigTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRegconfigTypeMapping.cs deleted file mode 100644 index 831aa0ae82..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRegconfigTypeMapping.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlRegconfigTypeMapping : 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 NpgsqlRegconfigTypeMapping Default { 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 NpgsqlRegconfigTypeMapping() - : base("regconfig", typeof(uint), NpgsqlDbType.Regconfig) - { - } - - /// - /// 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 NpgsqlRegconfigTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Regconfig) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlRegconfigTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"'{EscapeSqlLiteral((string)value)}'"; - - private string EscapeSqlLiteral(string literal) - => Check.NotNull(literal, nameof(literal)).Replace("'", "''"); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRegdictionaryTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRegdictionaryTypeMapping.cs deleted file mode 100644 index 7f2dac5568..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRegdictionaryTypeMapping.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlRegdictionaryTypeMapping : 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 NpgsqlRegdictionaryTypeMapping Default { 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 NpgsqlRegdictionaryTypeMapping() - : base("regdictionary", typeof(uint), NpgsqlDbType.Oid) - { - } - - /// - /// 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 NpgsqlRegdictionaryTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Oid) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlRegdictionaryTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"'{EscapeSqlLiteral((string)value)}'"; - - private string EscapeSqlLiteral(string literal) - => Check.NotNull(literal, nameof(literal)).Replace("'", "''"); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs deleted file mode 100644 index aeaadee6f8..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRowValueTypeMapping.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Data.Common; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// TODO: Update -/// Every node in the SQL tree must have a type mapping, but row values aren't actual values (in the sense that they can be sent as -/// parameters, or have a literal representation). So we have a dummy type mapping for that. -/// -public class NpgsqlRowValueTypeMapping : RelationalTypeMapping -{ - /// - /// 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 NpgsqlRowValueTypeMapping(Type clrType) - : base(new RelationalTypeMappingParameters(new CoreTypeMappingParameters(clrType), storeType: "record")) - { - } - - /// - /// 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 NpgsqlRowValueTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlRowValueTypeMapping(parameters); - - /// - /// 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. - /// - // SQL generation for row values is in NpgsqlQuerySqlGenerator - protected override string GenerateNonNullSqlLiteral(object value) - => throw new InvalidOperationException("GenerateNonNullSqlLiteral not supported on NpgsqlRowValueTypeMapping"); - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - => throw new InvalidOperationException("ConfigureParameter not supported on NpgsqlRowValueTypeMapping"); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs deleted file mode 100644 index 01c94af427..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Data.Common; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The base class for mapping Npgsql-specific string types. It configures parameters with the -/// provider-specific type enum. -/// -public class NpgsqlStringTypeMapping : StringTypeMapping, INpgsqlTypeMapping -{ - /// - /// 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 new NpgsqlStringTypeMapping Default { get; } = new("text", NpgsqlDbType.Text); - - /// - /// 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 NpgsqlDbType NpgsqlDbType { 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 NpgsqlStringTypeMapping(string storeType, NpgsqlDbType npgsqlDbType) - : base(storeType, System.Data.DbType.String) - { - NpgsqlDbType = npgsqlDbType; - } - - /// - /// 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 NpgsqlStringTypeMapping( - RelationalTypeMappingParameters parameters, - NpgsqlDbType npgsqlDbType) - : base(parameters) - { - NpgsqlDbType = npgsqlDbType; - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlStringTypeMapping(parameters, NpgsqlDbType); - - /// - /// 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 NpgsqlStringTypeMapping Clone(NpgsqlDbType npgsqlDbType) - => new(Parameters, npgsqlDbType); - - /// - /// 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 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}"); - } - - base.ConfigureParameter(parameter); - npgsqlParameter.NpgsqlDbType = NpgsqlDbType; - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs deleted file mode 100644 index eeb6413d5e..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStructuralJsonTypeMapping.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Data.Common; -using System.Text; -using System.Text.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// Supports the standard EF JSON support, which relies on owned entity or complex type modeling. -/// See for the older Npgsql-specific support, which allows mapping json/jsonb to text, to e.g. -/// (weakly-typed mapping) or to arbitrary POCOs (but without them being modeled). -/// -public class NpgsqlStructuralJsonTypeMapping : JsonTypeMapping -{ - /// - /// The database type used by Npgsql ( or . - /// - public virtual NpgsqlDbType NpgsqlDbType { get; } - - private static readonly MethodInfo GetStringMethod - = 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), [typeof(string)])!; - - private static readonly ConstructorInfo MemoryStreamConstructor - = typeof(MemoryStream).GetConstructor([typeof(byte[])])!; - - /// - /// 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 NpgsqlStructuralJsonTypeMapping(string storeType) - : base(storeType, typeof(JsonTypePlaceholder), dbType: null) - { - NpgsqlDbType = storeType switch - { - "json" => NpgsqlDbType.Json, - "jsonb" => NpgsqlDbType.Jsonb, - _ => throw new ArgumentException("Only the json and jsonb types are supported", nameof(storeType)) - }; - } - - /// - /// 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 MethodInfo GetDataReaderMethod() - => GetStringMethod; - - /// - /// 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 Expression CustomizeDataReaderExpression(Expression expression) - => Expression.New( - MemoryStreamConstructor, - Expression.Call( - Expression.Property(null, UTF8Property), - EncodingGetBytesMethod, - 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 NpgsqlStructuralJsonTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters) - { - NpgsqlDbType = npgsqlDbType; - } - - /// - /// 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 void ConfigureParameter(DbParameter parameter) - { - if (parameter is not NpgsqlParameter npgsqlParameter) - { - throw new InvalidOperationException( - $"Npgsql-specific type mapping {nameof(NpgsqlStructuralJsonTypeMapping)} being used with non-Npgsql parameter type {parameter.GetType().Name}"); - } - - base.ConfigureParameter(parameter); - npgsqlParameter.NpgsqlDbType = NpgsqlDbType; - } - - /// - /// 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 string EscapeSqlLiteral(string literal) - => literal.Replace("'", "''"); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"'{EscapeSqlLiteral((string)value)}'"; - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlStructuralJsonTypeMapping(parameters, NpgsqlDbType); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs deleted file mode 100644 index bcf13e7b40..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTidTypeMapping.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTidTypeMapping : 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 NpgsqlTidTypeMapping Default { 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 NpgsqlTidTypeMapping() - : base("tid", typeof(NpgsqlTid), NpgsqlDbType.Tid) - { - } - - /// - /// 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 NpgsqlTidTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Tid) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTidTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var tid = (NpgsqlTid)value; - var builder = new StringBuilder("TID '("); - builder.Append(tid.BlockNumber); - builder.Append(','); - builder.Append(tid.OffsetNumber); - builder.Append(")'"); - return builder.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var tid = (NpgsqlTid)value; - return Expression.New(Constructor, Expression.Constant(tid.BlockNumber), Expression.Constant(tid.OffsetNumber)); - } - - private static readonly ConstructorInfo Constructor = - typeof(NpgsqlTid).GetConstructor([typeof(uint), typeof(ushort)])!; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTypeMapping.cs deleted file mode 100644 index 25080e87b3..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTypeMapping.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Globalization; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTimeTypeMapping : 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 NpgsqlTimeTypeMapping Default { get; } = new(typeof(TimeOnly)); - - /// - /// 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 NpgsqlTimeTypeMapping(Type clrType) - : base( - "time without time zone", - clrType, - NpgsqlDbType.Time, - clrType == typeof(TimeOnly) - ? JsonTimeOnlyReaderWriter.Instance - : clrType == typeof(TimeSpan) - ? JsonTimeSpanReaderWriter.Instance - : throw new ArgumentException("clrType must be TimeOnly or TimeSpan", nameof(clrType))) - { - } - - /// - /// 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 NpgsqlTimeTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Time) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTimeTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null ? storeType : $"time({parameters.Precision}) without time zone"; - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"TIME '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => value switch - { - TimeSpan ts => ts.Ticks % 10000000 == 0 - ? ts.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture) - : ts.ToString(@"hh\:mm\:ss\.FFFFFF", CultureInfo.InvariantCulture), - TimeOnly t => t.Ticks % 10000000 == 0 - ? t.ToString(@"HH\:mm\:ss", CultureInfo.InvariantCulture) - : t.ToString(@"HH\:mm\:ss\.FFFFFF", CultureInfo.InvariantCulture), - _ => throw new InvalidCastException($"Can't generate a time SQL literal for CLR type {value.GetType()}") - }; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs deleted file mode 100644 index fad0f4143d..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimeTzTypeMapping.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTimeTzTypeMapping : 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 NpgsqlTimeTzTypeMapping Default { 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 NpgsqlTimeTzTypeMapping() - : base("time with time zone", typeof(DateTimeOffset), NpgsqlDbType.TimeTz, JsonTimeTzReaderWriter.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. - /// - protected NpgsqlTimeTzTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimeTz) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTimeTzTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null ? storeType : $"time({parameters.Precision}) with time zone"; - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => FormattableString.Invariant($"TIMETZ '{(DateTimeOffset)value:HH:mm:ss.FFFFFFz}'"); - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => FormattableString.Invariant(@$"{(DateTimeOffset)value:HH:mm:ss.FFFFFFz}"); - - /// - /// 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 - /// 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 JsonTimeTzReaderWriter Instance { get; } = new(); - - 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 deleted file mode 100644 index 21e3fccf04..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTimestampTypeMapping : 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 NpgsqlTimestampTypeMapping Default { 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 NpgsqlTimestampTypeMapping() - : base("timestamp without time zone", typeof(DateTime), NpgsqlDbType.Timestamp, NpgsqlJsonTimestampReaderWriter.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. - /// - protected NpgsqlTimestampTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Timestamp) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTimestampTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null ? storeType : $"timestamp({parameters.Precision}) without time zone"; - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMP '{GenerateLiteralCore(value)}'"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => $""" - "{GenerateLiteralCore(value)}" - """; - - private string GenerateLiteralCore(object value) - => FormatDateTime((DateTime)value); - - private static string FormatDateTime(DateTime dateTime) - { - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (dateTime == DateTime.MinValue) - { - return "-infinity"; - } - - if (dateTime == DateTime.MaxValue) - { - return "infinity"; - } - } - - return NpgsqlTypeMappingSource.LegacyTimestampBehavior || dateTime.Kind != DateTimeKind.Utc - ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) - : throw new ArgumentException("'timestamp without time zone' literal cannot be generated for a UTC DateTime"); - } - - /// - /// 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()!; - - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - switch (s) - { - case "-infinity": - return DateTime.MinValue; - case "infinity": - return DateTime.MaxValue; - } - } - - 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 deleted file mode 100644 index ef1ace2009..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTimestampTzTypeMapping : 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 NpgsqlTimestampTzTypeMapping Default { get; } = new(typeof(DateTime)); - - /// - /// 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 NpgsqlTimestampTzTypeMapping(Type clrType) - : base( - "timestamp with time zone", - clrType, - NpgsqlDbType.TimestampTz, - clrType == typeof(DateTime) - ? NpgsqlJsonTimestampTzDateTimeReaderWriter.Instance - : clrType == typeof(DateTimeOffset) - ? NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter.Instance - : throw new ArgumentException("clrType must be DateTime or DateTimeOffset", nameof(clrType))) - { - } - - /// - /// 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 NpgsqlTimestampTzTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TimestampTz) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTimestampTzTypeMapping(parameters); - - /// - /// 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 ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string _) - => parameters.Precision is null ? storeType : $"timestamp({parameters.Precision}) with time zone"; - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMPTZ '{Format(value)}'"; - - /// - /// 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 GenerateEmbeddedNonNullSqlLiteral(object value) - => $""" - "{Format(value)}" - """; - - private static string Format(object value) - => value switch - { - DateTime dateTime => Format(dateTime), - DateTimeOffset dateTimeOffset => Format(dateTimeOffset), - _ => throw new InvalidCastException( - $"Attempted to generate timestamptz literal for type {value.GetType()}, only DateTime and DateTimeOffset are supported") - }; - - private static string Format(DateTime dateTime) - { - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (dateTime == DateTime.MinValue) - { - return "-infinity"; - } - - if (dateTime == DateTime.MaxValue) - { - return "infinity"; - } - } - - return dateTime.Kind switch - { - DateTimeKind.Utc => dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z', - - DateTimeKind.Unspecified => NpgsqlTypeMappingSource.LegacyTimestampBehavior || dateTime == default - ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z' - : throw new ArgumentException( - $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), - - DateTimeKind.Local => NpgsqlTypeMappingSource.LegacyTimestampBehavior - ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture) - : throw new ArgumentException( - $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), - - _ => throw new UnreachableException() - }; - } - - private static string Format(DateTimeOffset dateTimeOffset) - { - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (dateTimeOffset == DateTimeOffset.MinValue) - { - return "-infinity"; - } - - if (dateTimeOffset == DateTimeOffset.MaxValue) - { - return "infinity"; - } - } - - return dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", 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 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()!; - - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - switch (s) - { - case "-infinity": - return DateTime.MinValue; - case "infinity": - return DateTime.MaxValue; - } - } - - // Our JSON string representation ends with Z (UTC), but DateTime.Parse returns a Local timestamp even in that case. Convert - // it in order to return a DateTime with Kind UTC. - 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); - } - - /// - /// 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()!; - - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - switch (s) - { - case "-infinity": - return DateTimeOffset.MinValue; - case "infinity": - return DateTimeOffset.MaxValue; - } - } - - 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/NpgsqlTsQueryTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsQueryTypeMapping.cs deleted file mode 100644 index ceb63f7a7b..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsQueryTypeMapping.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTsQueryTypeMapping : 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 NpgsqlTsQueryTypeMapping Default { 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 NpgsqlTsQueryTypeMapping() - : base("tsquery", typeof(NpgsqlTsQuery), NpgsqlDbType.TsQuery) - { - } - - /// - /// 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 NpgsqlTsQueryTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TsQuery) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTsQueryTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - Check.NotNull(value, nameof(value)); - var query = (NpgsqlTsQuery)value; - var builder = new StringBuilder(); - builder.Append("TSQUERY "); - var indexOfFirstQuote = builder.Length - 1; - query.Write(builder); - builder.Replace("'", "''"); - builder[indexOfFirstQuote] = '\''; - builder.Append("'"); - return builder.ToString(); - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsRankingNormalizationTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsRankingNormalizationTypeMapping.cs deleted file mode 100644 index 5ef2b01adb..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsRankingNormalizationTypeMapping.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTsRankingNormalizationTypeMapping : IntTypeMapping -{ - /// - /// 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 new NpgsqlTsRankingNormalizationTypeMapping Default { 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 NpgsqlTsRankingNormalizationTypeMapping() - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - typeof(NpgsqlTsRankingNormalization), new EnumToNumberConverter()), - "integer")) - { - } - - /// - /// 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 NpgsqlTsRankingNormalizationTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTsRankingNormalizationTypeMapping(parameters); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsVectorMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsVectorMapping.cs deleted file mode 100644 index 735c475789..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTsVectorMapping.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlTsVectorTypeMapping : 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 NpgsqlTsVectorTypeMapping Default { 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 NpgsqlTsVectorTypeMapping() - : base("tsvector", typeof(NpgsqlTsVector), NpgsqlDbType.TsVector) - { - } - - /// - /// 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 NpgsqlTsVectorTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.TsVector) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlTsVectorTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - Check.NotNull(value, nameof(value)); - var vector = (NpgsqlTsVector)value; - var builder = new StringBuilder(); - builder.Append("TSVECTOR "); - var indexOfFirstQuote = builder.Length - 1; - builder.Append(vector); - builder.Replace("'", "''"); - builder[indexOfFirstQuote] = '\''; - builder.Append("'"); - return builder.ToString(); - } -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTypeMapping.cs deleted file mode 100644 index 6b9bd92dcb..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTypeMapping.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Data.Common; -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -/// -/// The base class for mapping Npgsql-specific types. It configures parameters with the -/// provider-specific type enum. -/// -public abstract class NpgsqlTypeMapping : RelationalTypeMapping, INpgsqlTypeMapping -{ - /// - public virtual NpgsqlDbType NpgsqlDbType { get; } - - // ReSharper disable once PublicConstructorInAbstractClass - /// - /// Constructs an instance of the class. - /// - /// The database type to map. - /// The CLR type to map. - /// The database type used by Npgsql. - /// Handles reading and writing JSON values for instances of the mapped type. - /// If this type mapping represents a primitive collection, this holds the element's type mapping. - public NpgsqlTypeMapping( - string storeType, - Type clrType, - NpgsqlDbType npgsqlDbType, - JsonValueReaderWriter? jsonValueReaderWriter = null, - CoreTypeMapping? elementTypeMapping = null) - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - clrType, - jsonValueReaderWriter: jsonValueReaderWriter, - elementMapping: elementTypeMapping), - storeType)) - { - NpgsqlDbType = npgsqlDbType; - } - - /// - /// Constructs an instance of the class. - /// - /// The parameters for this mapping. - /// The database type of the range subtype. - protected NpgsqlTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters) - { - NpgsqlDbType = npgsqlDbType; - } - - /// - /// 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 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}"); - } - - base.ConfigureParameter(parameter); - npgsqlParameter.NpgsqlDbType = NpgsqlDbType; - } - - /// - /// Generates the SQL representation of a literal value meant to be embedded in another literal value, e.g. in a range. - /// - /// The literal value. - /// - /// The generated string. - /// - public virtual string GenerateEmbeddedSqlLiteral(object? value) - { - value = ConvertUnderlyingEnumValueToEnum(value); - - if (Converter != null) - { - value = Converter.ConvertToProvider(value); - } - - return GenerateEmbeddedProviderValueSqlLiteral(value); - } - - /// - /// Generates the SQL representation of a literal value without conversion, meant to be embedded in another literal value, - /// e.g. in a range. - /// - /// The literal value. - /// - /// The generated string. - /// - public virtual string GenerateEmbeddedProviderValueSqlLiteral(object? value) - => value == null - ? "NULL" - : GenerateEmbeddedNonNullSqlLiteral(value); - - /// - /// Generates the SQL representation of a non-null literal value, meant to be embedded in another literal value, e.g. in a range. - /// - /// The literal value. - /// - /// The generated string. - /// - protected virtual string GenerateEmbeddedNonNullSqlLiteral(object value) - => GenerateNonNullSqlLiteral(value); - - // Copied from RelationalTypeMapping - private object? ConvertUnderlyingEnumValueToEnum(object? value) - => value?.GetType().IsInteger() == true && ClrType.UnwrapNullableType().IsEnum - ? Enum.ToObject(ClrType.UnwrapNullableType(), value) - : value; -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs deleted file mode 100644 index 6b6028672e..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlUIntTypeMapping.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 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 - /// 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 NpgsqlUIntTypeMapping(string storeType, NpgsqlDbType npgsqlDbType) - : base(storeType, typeof(uint), npgsqlDbType, JsonUInt32ReaderWriter.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. - /// - protected NpgsqlUIntTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters, npgsqlDbType) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlUIntTypeMapping(parameters, NpgsqlDbType); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs deleted file mode 100644 index ef52a1e204..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlULongTypeMapping.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.EntityFrameworkCore.Storage.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlULongTypeMapping : 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 NpgsqlULongTypeMapping Default { get; } = new("xid8", NpgsqlDbType.Xid8); - - /// - /// 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 NpgsqlULongTypeMapping(string storeType, NpgsqlDbType npgsqlDbType) - : base(storeType, typeof(ulong), npgsqlDbType, JsonUInt64ReaderWriter.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. - /// - protected NpgsqlULongTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters, npgsqlDbType) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlULongTypeMapping(parameters, NpgsqlDbType); -} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs deleted file mode 100644 index 909475cc27..0000000000 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlVarbitTypeMapping.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Collections; -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Json; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.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 -/// 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 NpgsqlVarbitTypeMapping : 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 NpgsqlVarbitTypeMapping Default { 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 NpgsqlVarbitTypeMapping() - : base("bit varying", typeof(BitArray), NpgsqlDbType.Varbit, jsonValueReaderWriter: JsonBitArrayReaderWriter.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. - /// - protected NpgsqlVarbitTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters, NpgsqlDbType.Varbit) - { - } - - /// - /// 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 RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlVarbitTypeMapping(parameters); - - /// - /// 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 GenerateNonNullSqlLiteral(object value) - { - var bits = (BitArray)value; - var sb = new StringBuilder(); - sb.Append("B'"); - for (var i = 0; i < bits.Count; i++) - { - sb.Append(bits[i] ? '1' : '0'); - } - - sb.Append('\''); - return sb.ToString(); - } - - /// - /// 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 Expression GenerateCodeLiteral(object value) - { - var bits = (BitArray)value; - var exprs = new Expression[bits.Count]; - for (var i = 0; i < bits.Count; i++) - { - exprs[i] = Expression.Constant(bits[i]); - } - - return Expression.New( - Constructor, - Expression.NewArrayInit(typeof(bool), exprs)); - } - - private static readonly ConstructorInfo Constructor = - typeof(BitArray).GetConstructor([typeof(bool[])])!; -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs deleted file mode 100644 index ec07ae1bd7..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs +++ /dev/null @@ -1,202 +0,0 @@ -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 a data source builder action is specified, always create a data source. - { DataSourceBuilderAction: not null } o => GetSingletonDataSource(o), - - // If the user hasn't configured anything in UseNpgsql (no data source, no data source builder action, no connection, - // no connection string), check the application service provider to see if a data source is registered there, and return that. - // Otherwise if there's no connection string, abort: either a connection string or DataSourceBuilderAction is required - // to create a data source in any case. - { ConnectionString: null } or null - => applicationServiceProvider?.GetService() is DbDataSource dataSource - ? dataSource - : null, - - // The following are features which require an NpgsqlDataSource, since they require configuration on NpgsqlDataSourceBuilder. - { EnumDefinitions.Count: > 0 } o => GetSingletonDataSource(o), - { } o when _plugins.Any() => GetSingletonDataSource(o), - - // 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 at the EF level), allowing switching - // connection strings with the same service provider etc. - _ => null - }; - - private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension) - { - var connectionString = npgsqlOptionsExtension.ConnectionString; - - // It should be possible to use ConfigureDataSource() without supplying a connection string, providing the connection - // information via the connection string builder on the NpgsqlDataSourceBuilder. In order to support this, we - // coalesce null connection strings to empty strings (since dictionaries don't allow null keys). - // This is in line with general ADO.NET practice of coalescing null connection strings to empty strings. - connectionString ??= string.Empty; - - 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 deleted file mode 100644 index 5a3a79ad30..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System.Net.Sockets; -using System.Transactions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlDatabaseCreator( - RelationalDatabaseCreatorDependencies dependencies, - INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder) - : 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 - /// 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 TimeSpan RetryDelay { get; set; } = TimeSpan.FromMilliseconds(500); - - /// - /// 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 TimeSpan RetryTimeout { get; set; } = TimeSpan.FromMinutes(1); - - /// - /// 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 Create() - { - using (var masterConnection = connection.CreateAdminConnection()) - { - try - { - Dependencies.MigrationCommandExecutor - .ExecuteNonQuery(CreateCreateOperations(), masterConnection); - } - catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_database_datname_index" }) - { - // This occurs when two connections are trying to create the same database concurrently - // (happens in the tests). Simply ignore the error. - } - - ClearPool(); - } - - Exists(); - } - - /// - /// 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 CreateAsync(CancellationToken cancellationToken = default) - { - var masterConnection = connection.CreateAdminConnection(); - await using (masterConnection.ConfigureAwait(false)) - { - try - { - await Dependencies.MigrationCommandExecutor - .ExecuteNonQueryAsync(CreateCreateOperations(), masterConnection, cancellationToken) - .ConfigureAwait(false); - } - catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_database_datname_index" }) - { - // This occurs when two connections are trying to create the same database concurrently - // (happens in the tests). Simply ignore the error. - } - - ClearPool(); - } - - await ExistsAsync(cancellationToken).ConfigureAwait(false); - } - - /// - /// 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 bool HasTables() - => Dependencies.ExecutionStrategy - .Execute( - connection, - connection => (bool)CreateHasTablesCommand() - .ExecuteScalar( - new RelationalCommandParameterObject( - connection, - null, - null, - Dependencies.CurrentContext.Context, - Dependencies.CommandLogger))!); - - /// - /// 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 Task HasTablesAsync(CancellationToken cancellationToken = default) - => Dependencies.ExecutionStrategy.ExecuteAsync( - connection, - async (connection, ct) => (bool)(await CreateHasTablesCommand() - .ExecuteScalarAsync( - new RelationalCommandParameterObject( - connection, - null, - null, - Dependencies.CurrentContext.Context, - Dependencies.CommandLogger), - cancellationToken: ct).ConfigureAwait(false))!, cancellationToken); - - private IRelationalCommand CreateHasTablesCommand() - => rawSqlCommandBuilder - .Build( - """ -SELECT CASE WHEN COUNT(*) = 0 THEN FALSE ELSE TRUE END -FROM pg_class AS cls -JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace -WHERE - cls.relkind IN ('r', 'v', 'm', 'f', 'p') AND - ns.nspname NOT IN ('pg_catalog', 'information_schema') AND - -- Exclude tables which are members of PG extensions - NOT EXISTS ( - SELECT 1 FROM pg_depend WHERE - classid=( - SELECT cls.oid - FROM pg_class AS cls - JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace - WHERE relname='pg_class' AND ns.nspname='pg_catalog' - ) AND - objid=cls.oid AND - deptype IN ('e', 'x') - ) -"""); - - private IReadOnlyList CreateCreateOperations() - { - var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; - - return Dependencies.MigrationsSqlGenerator.Generate( - [ - new NpgsqlCreateDatabaseOperation - { - Name = connection.DbConnection.Database, - Template = designTimeModel.GetDatabaseTemplate(), - Collation = designTimeModel.GetCollation(), - Tablespace = designTimeModel.GetTablespace() - } - ]); - } - - /// - /// 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 bool Exists() - => Dependencies.ExecutionStrategy.Execute(() => - { - using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); - var opened = false; - - try - { - connection.Open(errorsExpected: true); - opened = true; - - rawSqlCommandBuilder - .Build("SELECT 1") - .ExecuteNonQuery( - new RelationalCommandParameterObject( - connection, - parameterValues: null, - readerColumns: null, - Dependencies.CurrentContext.Context, - Dependencies.CommandLogger, - CommandSource.Migrations)); - - return true; - } - catch (Exception e) when (IsDoesNotExist(e)) - { - return false; - } - finally - { - if (opened) - { - connection.Close(); - } - } - }); - - /// - /// 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 Task ExistsAsync(CancellationToken cancellationToken = default) - => Dependencies.ExecutionStrategy.ExecuteAsync(async ct => - { - using var _ = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); - var opened = false; - - try - { - await connection.OpenAsync(cancellationToken, errorsExpected: true).ConfigureAwait(false); - opened = true; - - await rawSqlCommandBuilder - .Build("SELECT 1") - .ExecuteNonQueryAsync( - new RelationalCommandParameterObject( - connection, - parameterValues: null, - readerColumns: null, - Dependencies.CurrentContext.Context, - Dependencies.CommandLogger, - CommandSource.Migrations), - cancellationToken) - .ConfigureAwait(false); - - return true; - } - catch (Exception e) when (IsDoesNotExist(e)) - { - return false; - } - finally - { - if (opened) - { - await connection.CloseAsync().ConfigureAwait(false); - } - } - }, cancellationToken); - - private static bool IsDoesNotExist(Exception exception) - => exception switch - { - // Login failed is thrown when database does not exist (See Issue #776) - PostgresException { SqlState: "3D000" } - => true, - - // This can happen when Npgsql attempts to connect to multiple hosts - NpgsqlException { InnerException: AggregateException ae } when ae.InnerExceptions.Any(ie => ie is PostgresException { SqlState: "3D000" }) - => true, - - // Pretty awful hack around #104 - NpgsqlException { InnerException: IOException { InnerException: SocketException { SocketErrorCode: SocketError.ConnectionReset } } } - => true, - - _ => false - }; - - /// - /// 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 Delete() - { - switch (connection.DataSource) - { - case NpgsqlDataSource dataSource: - dataSource.Clear(); - break; - case null: - ClearAllPools(); - break; - } - - using (var masterConnection = connection.CreateAdminConnection()) - { - Dependencies.MigrationCommandExecutor - .ExecuteNonQuery(CreateDropCommands(), masterConnection); - } - } - - /// - /// 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 DeleteAsync(CancellationToken cancellationToken = default) - { - 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(); - await using (masterConnection) - { - await Dependencies.MigrationCommandExecutor - .ExecuteNonQueryAsync(CreateDropCommands(), masterConnection, cancellationToken) - .ConfigureAwait(false); - } - } - - /// - /// 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 CreateTables() - { - var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; - var operations = Dependencies.ModelDiffer.GetDifferences(null, designTimeModel.GetRelationalModel()); - var commands = Dependencies.MigrationsSqlGenerator.Generate(operations, designTimeModel); - - // If a PostgreSQL extension, enum or range was added, we want Npgsql to reload all types at the ADO.NET level. - var reloadTypes = - operations.OfType() - .Any( - o => - o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); - - try - { - Dependencies.MigrationCommandExecutor.ExecuteNonQuery(commands, connection); - } - catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_type_typname_nsp_index" }) - { - // This occurs when two connections are trying to create the same database concurrently - // (happens in the tests). Simply ignore the error. - } - - if (reloadTypes && connection.DbConnection is NpgsqlConnection npgsqlConnection) - { - npgsqlConnection.Open(); - try - { - npgsqlConnection.ReloadTypes(); - } - finally - { - npgsqlConnection.Close(); - } - } - } - - /// - /// 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 CreateTablesAsync(CancellationToken cancellationToken = default) - { - var designTimeModel = Dependencies.CurrentContext.Context.GetService().Model; - var operations = Dependencies.ModelDiffer.GetDifferences(null, designTimeModel.GetRelationalModel()); - var commands = Dependencies.MigrationsSqlGenerator.Generate(operations, designTimeModel); - - try - { - await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(commands, connection, cancellationToken) - .ConfigureAwait(false); - } - catch (PostgresException e) when (e is { SqlState: "23505", ConstraintName: "pg_type_typname_nsp_index" }) - { - // This occurs when two connections are trying to create the same database concurrently - // (happens in the tests). Simply ignore the error. - } - - // If a PostgreSQL extension, enum or range was added, we want Npgsql to reload all types at the ADO.NET level. - var reloadTypes = operations - .OfType() - .Any(o => o.GetPostgresExtensions().Any() || o.GetPostgresEnums().Any() || o.GetPostgresRanges().Any()); - - if (reloadTypes && connection.DbConnection is NpgsqlConnection npgsqlConnection) - { - await npgsqlConnection.OpenAsync(cancellationToken).ConfigureAwait(false); - try - { - await npgsqlConnection.ReloadTypesAsync(cancellationToken).ConfigureAwait(false); - } - finally - { - await npgsqlConnection.CloseAsync().ConfigureAwait(false); - } - } - } - - private IReadOnlyList CreateDropCommands() - { - var operations = new MigrationOperation[] - { - // TODO Check DbConnection.Database always gives us what we want - // Issue #775 - new NpgsqlDropDatabaseOperation { Name = connection.DbConnection.Database } - }; - - return Dependencies.MigrationsSqlGenerator.Generate(operations); - } - - // Clear connection pools in case there are active connections that are pooled - private static void ClearAllPools() - => NpgsqlConnection.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); -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlExecutionStrategy.cs b/src/EFCore.PG/Storage/Internal/NpgsqlExecutionStrategy.cs deleted file mode 100644 index 3aabcce3cf..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlExecutionStrategy.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlExecutionStrategy : IExecutionStrategy -{ - private ExecutionStrategyDependencies Dependencies { 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 NpgsqlExecutionStrategy(ExecutionStrategyDependencies dependencies) - { - Dependencies = 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 - /// 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 bool RetriesOnFailure - => false; - - /// - /// 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 TResult Execute( - TState state, - Func operation, - Func>? verifySucceeded) - { - try - { - return operation(Dependencies.CurrentContext.Context, state); - } - catch (Exception ex) when (ExecutionStrategy.CallOnWrappedException(ex, NpgsqlTransientExceptionDetector.ShouldRetryOn)) - { - throw new InvalidOperationException("An exception has been raised that is likely due to a transient failure.", ex); - } - } - - /// - /// 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 async Task ExecuteAsync( - TState state, - Func> operation, - Func>>? verifySucceeded, - CancellationToken cancellationToken) - { - try - { - return await operation(Dependencies.CurrentContext.Context, state, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) when (ExecutionStrategy.CallOnWrappedException(ex, NpgsqlTransientExceptionDetector.ShouldRetryOn)) - { - throw new InvalidOperationException("An exception has been raised that is likely due to a transient failure.", ex); - } - } -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlExecutionStrategyFactory.cs b/src/EFCore.PG/Storage/Internal/NpgsqlExecutionStrategyFactory.cs deleted file mode 100644 index fc630f0035..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlExecutionStrategyFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlExecutionStrategyFactory : RelationalExecutionStrategyFactory -{ - /// - /// 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 NpgsqlExecutionStrategyFactory( - ExecutionStrategyDependencies dependencies) - : base(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 - /// 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 IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) - => new NpgsqlExecutionStrategy(dependencies); -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs deleted file mode 100644 index 89df883b0d..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Net.Security; -using System.Transactions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlRelationalConnection : RelationalConnection, INpgsqlRelationalConnection -{ - private readonly ProvideClientCertificatesCallback? _provideClientCertificatesCallback; - private readonly RemoteCertificateValidationCallback? _remoteCertificateValidationCallback; - -#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete - private readonly ProvidePasswordCallback? _providePasswordCallback; -#pragma warning restore CS0618 - - /// - /// 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 - /// - protected override bool SupportsAmbientTransactions - => 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. - /// - public NpgsqlRelationalConnection( - RelationalConnectionDependencies dependencies, - NpgsqlDataSourceManager dataSourceManager, - IDbContextOptions options) - : this( - dependencies, - dataSourceManager.GetDataSource( - options.FindExtension(), - options.FindExtension()?.ApplicationServiceProvider)) - { - } - - /// - /// 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 NpgsqlRelationalConnection(RelationalConnectionDependencies dependencies, DbDataSource? dataSource) - : base(dependencies) - { - if (dataSource is not null) - { - DataSource = dataSource; - -#if DEBUG - // We validate in NpgsqlOptionsExtensions.Validate that DataSource and these callbacks aren't specified together - if (dependencies.ContextOptions.FindExtension() is { } npgsqlOptions) - { - Check.DebugAssert( - npgsqlOptions?.ProvideClientCertificatesCallback is null, - "Both DataSource and ProvideClientCertificatesCallback are non-null"); - Check.DebugAssert( - npgsqlOptions?.RemoteCertificateValidationCallback is null, - "Both DataSource and RemoteCertificateValidationCallback are non-null"); - Check.DebugAssert( - npgsqlOptions?.ProvidePasswordCallback is null, - "Both DataSource and ProvidePasswordCallback are non-null"); - } -#endif - } - else if (dependencies.ContextOptions.FindExtension() is { } npgsqlOptions) - { - _provideClientCertificatesCallback = npgsqlOptions.ProvideClientCertificatesCallback; - _remoteCertificateValidationCallback = npgsqlOptions.RemoteCertificateValidationCallback; - _providePasswordCallback = npgsqlOptions.ProvidePasswordCallback; - } - } - - /// - /// 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 DbConnection CreateDbConnection() - { - if (DataSource is not null) - { - return DataSource.CreateConnection(); - } - - var conn = new NpgsqlConnection(ConnectionString); - - if (_provideClientCertificatesCallback is not null || _remoteCertificateValidationCallback is not null) - { - conn.SslClientAuthenticationOptionsCallback = o => - { - if (_provideClientCertificatesCallback is not null) - { - o.ClientCertificates ??= new(); - _provideClientCertificatesCallback(o.ClientCertificates); - } - - o.RemoteCertificateValidationCallback = _remoteCertificateValidationCallback; - }; - } - - if (_providePasswordCallback is not null) - { -#pragma warning disable 618 // ProvidePasswordCallback is obsolete - conn.ProvidePasswordCallback = _providePasswordCallback; -#pragma warning restore 618 - } - - return conn; - } - - /// - /// 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. - /// - // 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; - set - { - base.ConnectionString = value; - - DataSource = 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. - /// - [AllowNull] - public new virtual NpgsqlConnection DbConnection - { - get => (NpgsqlConnection)base.DbConnection; - set - { - base.DbConnection = value; - - DataSource = 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 virtual DbDataSource? DbDataSource - { - get => DataSource; - set - { - DbConnection = null; - ConnectionString = null; - DataSource = value; - } - } - - /// - /// 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 INpgsqlRelationalConnection CreateAdminConnection() - { - if (Dependencies.ContextOptions.FindExtension() is not { } npgsqlOptions) - { - throw new InvalidOperationException($"{nameof(NpgsqlOptionsExtension)} not found in {nameof(CreateAdminConnection)}"); - } - - var adminConnectionString = new NpgsqlConnectionStringBuilder(ConnectionString) - { - Database = npgsqlOptions.AdminDatabase ?? "postgres", - Pooling = false, - Multiplexing = false - }.ToString(); - - var adminNpgsqlOptions = DataSource is not null - ? npgsqlOptions.WithConnection(((NpgsqlConnection)CreateDbConnection()).CloneWith(adminConnectionString)) - : npgsqlOptions.Connection is not null - ? npgsqlOptions.WithConnection(DbConnection.CloneWith(adminConnectionString)) - : npgsqlOptions.WithConnectionString(adminConnectionString); - - var optionsBuilder = new DbContextOptionsBuilder(); - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(adminNpgsqlOptions); - - return new NpgsqlRelationalConnection(Dependencies with { ContextOptions = optionsBuilder.Options }, dataSource: 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. - /// - // Accessing Transaction.Current is expensive, so don't do it if Enlist is false in the connection string - public override Transaction? CurrentAmbientTransaction - => ConnectionString is null || !ConnectionString.Contains("Enlist=false", StringComparison.InvariantCultureIgnoreCase) - ? Transaction.Current - : 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 virtual async ValueTask CloneWith( - string connectionString, - bool async, - CancellationToken cancellationToken = default) - { - var clonedDbConnection = async - ? await DbConnection.CloneWithAsync(connectionString, cancellationToken).ConfigureAwait(false) - : DbConnection.CloneWith(connectionString); - - var relationalOptions = RelationalOptionsExtension.Extract(Dependencies.ContextOptions) - .WithConnectionString(null) - .WithConnection(clonedDbConnection); - - var optionsBuilder = new DbContextOptionsBuilder(); - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(relationalOptions); - - return new NpgsqlRelationalConnection(Dependencies with { ContextOptions = optionsBuilder.Options }, dataSource: null); - } -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs b/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs deleted file mode 100644 index 89eddbdc0c..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Data; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.RegularExpressions; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlSqlGenerationHelper : RelationalSqlGenerationHelper -{ - private static readonly HashSet ReservedWords; - - static NpgsqlSqlGenerationHelper() - { - // https://www.postgresql.org/docs/current/static/sql-keywords-appendix.html - using (var conn = new NpgsqlConnection()) - { - ReservedWords = - [..conn.GetSchema("ReservedWords").Rows.Cast().Select(r => (string)r["ReservedWord"])]; - } - } - - /// - /// 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 NpgsqlSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies) - : base(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 - /// 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 string DelimitIdentifier(string identifier) - => RequiresQuoting(identifier) ? base.DelimitIdentifier(identifier) : identifier; - - /// - /// 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 DelimitIdentifier(StringBuilder builder, string identifier) - { - if (RequiresQuoting(identifier)) - { - base.DelimitIdentifier(builder, identifier); - } - else - { - builder.Append(identifier); - } - } - - /// - /// Returns whether the given string can be used as an unquoted identifier in PostgreSQL, without quotes. - /// - private static bool RequiresQuoting(string identifier) - { - var first = identifier[0]; - if (!char.IsLower(first) && first != '_') - { - return true; - } - - for (var i = 1; i < identifier.Length; i++) - { - var c = identifier[i]; - - if (char.IsLower(c)) - { - continue; - } - - switch (c) - { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '_': - case '$': // yes it's true - continue; - } - - return true; - } - - if (ReservedWords.Contains(identifier.ToUpperInvariant())) - { - return true; - } - - return false; - } -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTransientExceptionDetector.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTransientExceptionDetector.cs deleted file mode 100644 index 6bb298eaf4..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTransientExceptionDetector.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlTransientExceptionDetector -{ - /// - /// 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 ShouldRetryOn(Exception? ex) - => (ex as NpgsqlException)?.IsTransient == true || ex is TimeoutException; -} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs deleted file mode 100644 index 4908b1ee86..0000000000 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ /dev/null @@ -1,1300 +0,0 @@ -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Data; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.NetworkInformation; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.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 NpgsqlTypeMappingSource : RelationalTypeMappingSource -{ -#if DEBUG - internal static bool LegacyTimestampBehavior; - - // ReSharper disable once FieldCanBeMadeReadOnly.Global - internal static bool DisableDateTimeInfinityConversions; -#else - internal static readonly bool LegacyTimestampBehavior; - internal static readonly bool DisableDateTimeInfinityConversions; -#endif - - static NpgsqlTypeMappingSource() - { - LegacyTimestampBehavior = AppContext.TryGetSwitch("Npgsql.EnableLegacyTimestampBehavior", out var enabled) && enabled; - DisableDateTimeInfinityConversions = AppContext.TryGetSwitch("Npgsql.DisableDateTimeInfinityConversions", out enabled) && enabled; - } - - private readonly ISqlGenerationHelper _sqlGenerationHelper; - - /// - /// 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 ConcurrentDictionary StoreTypeMappings { 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. - /// - protected virtual ConcurrentDictionary ClrTypeMappings { get; } - - private readonly IReadOnlyList _enumDefinitions; - private readonly IReadOnlyList _userRangeDefinitions; - - private readonly bool _supportsMultiranges; - - #region Mappings - - // Numeric types - private readonly NpgsqlFloatTypeMapping _float4 = NpgsqlFloatTypeMapping.Default; - private readonly NpgsqlDoubleTypeMapping _float8 = NpgsqlDoubleTypeMapping.Default; - private readonly NpgsqlDecimalTypeMapping _numeric = NpgsqlDecimalTypeMapping.Default; - private readonly NpgsqlBigIntegerTypeMapping _bigInteger = NpgsqlBigIntegerTypeMapping.Default; - private readonly NpgsqlDecimalTypeMapping _numericAsFloat = new(typeof(float)); - private readonly NpgsqlDecimalTypeMapping _numericAsDouble = new(typeof(double)); - private readonly NpgsqlMoneyTypeMapping _money = NpgsqlMoneyTypeMapping.Default; - private readonly GuidTypeMapping _uuid = new("uuid", DbType.Guid); - private readonly ShortTypeMapping _int2 = new("smallint", DbType.Int16); - private readonly ByteTypeMapping _int2Byte = new("smallint", DbType.Byte); - private readonly IntTypeMapping _int4 = new("integer", DbType.Int32); - private readonly LongTypeMapping _int8 = new("bigint", DbType.Int64); - - // Character types - private readonly StringTypeMapping _text = new("text", DbType.String); - private readonly NpgsqlStringTypeMapping _varchar = new("character varying", NpgsqlDbType.Varchar); - private readonly NpgsqlCharacterStringTypeMapping _char = new("character"); - 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 NpgsqlStructuralJsonTypeMapping _jsonbOwned = new("jsonb"); - private readonly NpgsqlStructuralJsonTypeMapping _jsonOwned = new("json"); - - // JSON mappings - older string/weakly-typed support - private readonly NpgsqlJsonTypeMapping _jsonbString = new("jsonb", typeof(string)); - private readonly NpgsqlJsonTypeMapping _jsonString = new("json", typeof(string)); - private readonly NpgsqlJsonTypeMapping _jsonbDocument = new("jsonb", typeof(JsonDocument)); - private readonly NpgsqlJsonTypeMapping _jsonDocument = new("json", typeof(JsonDocument)); - private readonly NpgsqlJsonTypeMapping _jsonbElement = new("jsonb", typeof(JsonElement)); - private readonly NpgsqlJsonTypeMapping _jsonElement = new("json", typeof(JsonElement)); - - // Date/Time types - private readonly NpgsqlDateTimeDateTypeMapping _dateDateTime = NpgsqlDateTimeDateTypeMapping.Default; - private readonly NpgsqlTimestampTypeMapping _timestamp = NpgsqlTimestampTypeMapping.Default; - private readonly NpgsqlTimestampTzTypeMapping _timestamptz = NpgsqlTimestampTzTypeMapping.Default; - private readonly NpgsqlTimestampTzTypeMapping _timestamptzDto = new(typeof(DateTimeOffset)); - private readonly NpgsqlIntervalTypeMapping _interval = NpgsqlIntervalTypeMapping.Default; - private readonly NpgsqlTimeTypeMapping _timeTimeSpan = new(typeof(TimeSpan)); - private readonly NpgsqlTimeTzTypeMapping _timetz = NpgsqlTimeTzTypeMapping.Default; - - private readonly NpgsqlDateOnlyTypeMapping _dateDateOnly = NpgsqlDateOnlyTypeMapping.Default; - private readonly NpgsqlTimeTypeMapping _timeTimeOnly = NpgsqlTimeTypeMapping.Default; - - // Network address types - private readonly NpgsqlMacaddrTypeMapping _macaddr = NpgsqlMacaddrTypeMapping.Default; - private readonly NpgsqlMacaddr8TypeMapping _macaddr8 = NpgsqlMacaddr8TypeMapping.Default; - private readonly NpgsqlInetTypeMapping _inetAsIPAddress = NpgsqlInetTypeMapping.Default; - private readonly NpgsqlInetTypeMapping _inetAsNpgsqlInet = new(typeof(NpgsqlInet)); - private readonly NpgsqlCidrTypeMapping _cidrAsIPNetwork = NpgsqlCidrTypeMapping.Default; - private readonly NpgsqlLegacyCidrTypeMapping _cidrAsNpgsqlCidr = NpgsqlLegacyCidrTypeMapping.Default; - - // Built-in geometric types - private readonly NpgsqlPointTypeMapping _point = NpgsqlPointTypeMapping.Default; - private readonly NpgsqlBoxTypeMapping _box = NpgsqlBoxTypeMapping.Default; - private readonly NpgsqlLineTypeMapping _line = NpgsqlLineTypeMapping.Default; - private readonly NpgsqlLineSegmentTypeMapping _lseg = NpgsqlLineSegmentTypeMapping.Default; - private readonly NpgsqlPathTypeMapping _path = NpgsqlPathTypeMapping.Default; - private readonly NpgsqlPolygonTypeMapping _polygon = NpgsqlPolygonTypeMapping.Default; - private readonly NpgsqlCircleTypeMapping _circle = NpgsqlCircleTypeMapping.Default; - - // uint/ulong mappings - private readonly NpgsqlUIntTypeMapping _xid = new("xid", NpgsqlDbType.Xid); - private readonly NpgsqlULongTypeMapping _xid8 = new("xid8", NpgsqlDbType.Xid8); - private readonly NpgsqlUIntTypeMapping _oid = new("oid", NpgsqlDbType.Oid); - private readonly NpgsqlUIntTypeMapping _cid = new("cid", NpgsqlDbType.Cid); - private readonly NpgsqlUIntTypeMapping _regtype = new("regtype", NpgsqlDbType.Regtype); - private readonly NpgsqlUIntTypeMapping _lo = new("lo", NpgsqlDbType.Oid); - - // Full text search mappings - private readonly NpgsqlTsQueryTypeMapping _tsquery = NpgsqlTsQueryTypeMapping.Default; - private readonly NpgsqlTsVectorTypeMapping _tsvector = NpgsqlTsVectorTypeMapping.Default; - private readonly NpgsqlRegconfigTypeMapping _regconfig = NpgsqlRegconfigTypeMapping.Default; - private readonly NpgsqlTsRankingNormalizationTypeMapping _rankingNormalization = NpgsqlTsRankingNormalizationTypeMapping.Default; - - // Unaccent mapping - private readonly NpgsqlRegdictionaryTypeMapping _regdictionary = new(); - - // Built-in ranges - // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable - private readonly NpgsqlRangeTypeMapping _int4range; - private readonly NpgsqlRangeTypeMapping _int8range; - private readonly NpgsqlRangeTypeMapping _numrange; - private readonly NpgsqlRangeTypeMapping _tsrange; - private readonly NpgsqlRangeTypeMapping _tstzrange, _tstzrangeDto; - private readonly NpgsqlRangeTypeMapping _dateOnlyDaterange; - private readonly NpgsqlRangeTypeMapping _dateTimeDaterange; - - // Other types - private readonly NpgsqlBoolTypeMapping _bool = NpgsqlBoolTypeMapping.Default; - private readonly NpgsqlBitTypeMapping _bit = NpgsqlBitTypeMapping.Default; - private readonly NpgsqlVarbitTypeMapping _varbit = NpgsqlVarbitTypeMapping.Default; - private readonly NpgsqlByteArrayTypeMapping _bytea = NpgsqlByteArrayTypeMapping.Default; - private readonly NpgsqlHstoreTypeMapping _hstore = NpgsqlHstoreTypeMapping.Default; - private readonly NpgsqlHstoreTypeMapping _immutableHstore = new(typeof(ImmutableDictionary)); - private readonly NpgsqlTidTypeMapping _tid = NpgsqlTidTypeMapping.Default; - private readonly NpgsqlPgLsnTypeMapping _pgLsn = NpgsqlPgLsnTypeMapping.Default; - - private readonly NpgsqlLTreeTypeMapping _ltree = NpgsqlLTreeTypeMapping.Default; - private readonly NpgsqlStringTypeMapping _ltreeString = new("ltree", NpgsqlDbType.LTree); - private readonly NpgsqlStringTypeMapping _lquery = new("lquery", NpgsqlDbType.LQuery); - private readonly NpgsqlStringTypeMapping _ltxtquery = new("ltxtquery", NpgsqlDbType.LTxtQuery); - - private readonly NpgsqlCubeTypeMapping _cube = NpgsqlCubeTypeMapping.Default; - - /// - /// 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. - /// - // Special stuff - // ReSharper disable once InconsistentNaming - public readonly NpgsqlEStringTypeMapping EStringTypeMapping = NpgsqlEStringTypeMapping.Default; - - #endregion Mappings - - /// - /// 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 NpgsqlTypeMappingSource( - TypeMappingSourceDependencies dependencies, - RelationalTypeMappingSourceDependencies relationalDependencies, - ISqlGenerationHelper sqlGenerationHelper, - INpgsqlSingletonOptions options) - : base(dependencies, relationalDependencies) - { - _supportsMultiranges = options.PostgresVersion.AtLeast(14); - _sqlGenerationHelper = Check.NotNull(sqlGenerationHelper, nameof(sqlGenerationHelper)); - - // Initialize range mappings, which reference on other mappings - _int4range = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "int4range", typeof(NpgsqlRange), NpgsqlDbType.IntegerRange, _int4); - _int8range = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "int8range", typeof(NpgsqlRange), NpgsqlDbType.BigIntRange, _int8); - _numrange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "numrange", typeof(NpgsqlRange), NpgsqlDbType.NumericRange, _numeric); - _tsrange = NpgsqlRangeTypeMapping.CreatBuiltInRangeMapping( - "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( - "daterange", typeof(NpgsqlRange), NpgsqlDbType.DateRange, _dateDateTime); - -// ReSharper disable CoVariantArrayConversion - // Note that PostgreSQL has aliases to some built-in type name aliases (e.g. int4 for integer), - // these are mapped as well. - // https://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE - var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "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", [_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", [_cidrAsIPNetwork, _cidrAsNpgsqlCidr] }, - { "point", [_point] }, - { "box", [_box] }, - { "line", [_line] }, - { "lseg", [_lseg] }, - { "path", [_path] }, - { "polygon", [_polygon] }, - { "circle", [_circle] }, - { "cube", [_cube] }, - { "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 - - // Set up aliases - storeTypeMappings["timestamp"] = storeTypeMappings["timestamp without time zone"]; - storeTypeMappings["timestamptz"] = storeTypeMappings["timestamp with time zone"]; - storeTypeMappings["time"] = storeTypeMappings["time without time zone"]; - storeTypeMappings["timetz"] = storeTypeMappings["time with time zone"]; - - var clrTypeMappings = new Dictionary - { - { typeof(bool), _bool }, - { typeof(Guid), _uuid }, - { typeof(byte), _int2Byte }, - { typeof(short), _int2 }, - { typeof(int), _int4 }, - { typeof(long), _int8 }, - { typeof(float), _float4 }, - { typeof(double), _float8 }, - { typeof(decimal), _numeric }, - { typeof(BigInteger), _bigInteger }, - { typeof(string), _text }, - { typeof(JsonDocument), _jsonbDocument }, - { typeof(JsonElement), _jsonbElement }, - { typeof(JsonTypePlaceholder), _jsonbOwned }, - { typeof(char), _singleChar }, - { typeof(DateTime), LegacyTimestampBehavior ? _timestamp : _timestamptz }, - { typeof(DateOnly), _dateDateOnly }, - { typeof(TimeOnly), _timeTimeOnly }, - { typeof(TimeSpan), _interval }, - { typeof(DateTimeOffset), _timestamptzDto }, - { typeof(PhysicalAddress), _macaddr }, - { typeof(IPAddress), _inetAsIPAddress }, - { typeof(NpgsqlInet), _inetAsNpgsqlInet }, - { typeof(IPNetwork), _cidrAsIPNetwork }, -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - { typeof(NpgsqlCidr), _cidrAsNpgsqlCidr }, -#pragma warning restore CS0618 - { typeof(BitArray), _varbit }, - { typeof(ImmutableDictionary), _immutableHstore }, - { typeof(Dictionary), _hstore }, - { typeof(NpgsqlTid), _tid }, - { typeof(NpgsqlLogSequenceNumber), _pgLsn }, - { typeof(NpgsqlPoint), _point }, - { typeof(NpgsqlBox), _box }, - { typeof(NpgsqlLine), _line }, - { typeof(NpgsqlLSeg), _lseg }, - { typeof(NpgsqlPath), _path }, - { typeof(NpgsqlPolygon), _polygon }, - { typeof(NpgsqlCircle), _circle }, - { typeof(NpgsqlCube), _cube }, - { typeof(NpgsqlRange), _int4range }, - { typeof(NpgsqlRange), _int8range }, - { typeof(NpgsqlRange), _numrange }, - { typeof(NpgsqlRange), LegacyTimestampBehavior ? _tsrange : _tstzrange }, - { typeof(NpgsqlRange), _tstzrange }, - { typeof(NpgsqlRange), _dateOnlyDaterange }, - { typeof(NpgsqlTsQuery), _tsquery }, - { typeof(NpgsqlTsVector), _tsvector }, - { typeof(NpgsqlTsRankingNormalization), _rankingNormalization }, - { typeof(LTree), _ltree } - }; - - StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); - ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); - - _enumDefinitions = options.EnumDefinitions; - _userRangeDefinitions = options.UserRangeDefinitions; - } - - /// - /// 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 RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) - // 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); - - /// - /// 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? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo) - { - var clrType = mappingInfo.ClrType; - var storeTypeName = mappingInfo.StoreTypeName; - var storeTypeNameBase = mappingInfo.StoreTypeNameBase; - - if (storeTypeName is not null) - { - if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings)) - { - // We found the user-specified store type. No CLR type was provided - we're probably - // scaffolding from an existing database, take the first mapping as the default. - if (clrType is null) - { - return mappings[0]; - } - - // A CLR type was provided - look for a mapping between the store and CLR types. If not found, fail - // immediately. - foreach (var m in mappings) - { - if (m.ClrType == clrType) - { - return m; - } - } - - // Map arbitrary user POCOs to JSON - if (storeTypeName is "jsonb" or "json") - { - return FindCollectionMapping( - mappingInfo, - mappingInfo.ClrType, - providerType: null, - elementMapping: mappingInfo.CoreTypeMappingInfo.ElementTypeMapping) - ?? new NpgsqlJsonTypeMapping(storeTypeName, clrType); - } - - return null; - } - - if (StoreTypeMappings.TryGetValue(storeTypeNameBase!, out mappings)) - { - if (clrType is null) - { - return mappings[0]; - } - - foreach (var m in mappings) - { - if (m.ClrType == clrType) - { - return m; - } - } - - return null; - } - - // 'character' is special: 'character' (no size) and 'character(1)' map to a single char, whereas 'character(n)' maps - // to a string - if (storeTypeNameBase is "character" or "char") - { - if (mappingInfo.Size is null or 1 && clrType is null || clrType == typeof(char)) - { - return _singleChar.Clone(mappingInfo); - } - - if (clrType is null || clrType == typeof(string)) - { - return _char.Clone(mappingInfo); - } - } - - 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 - RelationalTypeMapping collectionMapping) - { - return collectionMapping; - } - - // A store type name was provided, but is unknown. This could be a domain (alias) type, in which case - // we proceed with a CLR type lookup (if the type doesn't exist at all the failure will come later). - } - - if (clrType is not null) - { - if (ClrTypeMappings.TryGetValue(clrType, out var mapping)) - { - // Handle types with the size facet (string, bitarray) - if (mappingInfo.Size > 0) - { - if (clrType == typeof(string)) - { - mapping = mappingInfo.IsFixedLength ?? false ? _char : _varchar; - - // See #342 for when size > 10485760 - return mappingInfo.Size <= 10485760 - ? mapping.WithStoreTypeAndSize($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size) - : _text; - } - - if (clrType == typeof(BitArray)) - { - mapping = mappingInfo.IsFixedLength ?? false ? _bit : _varbit; - return mapping.WithStoreTypeAndSize($"{mapping.StoreType}({mappingInfo.Size})", mappingInfo.Size); - } - } - - return mapping; - } - - if (clrType == typeof(byte[]) && mappingInfo.ElementTypeMapping is null) - { - if (storeTypeName == "smallint[]") - { - // PostgreSQL has no tinyint (single-byte) type, but we allow mapping CLR byte to PG smallint (2-bytes). - // The same applies to arrays - as always - so byte[] should be mappable to smallint[]. - // However, byte[] also has a base mapping to bytea, which is the default. So when the user explicitly specified - // mapping to smallint[], we don't return that to allow the array mapping to work. - // TODO: This is a workaround; RelationalTypeMappingSource first attempts to find a value converter before trying - // to find a collection. We should reverse the order and call FindCollectionMapping before attempting to find a - // value converter. - // TODO: Make sure the providerType should be null - return FindCollectionMapping(mappingInfo, typeof(byte[]), providerType: null, elementMapping: null); - // return null; - } - - return _bytea; - } - - if (mappingInfo.IsRowVersion == true) - { - if (clrType == typeof(uint)) - { - return _xid; - } - - if (clrType == typeof(ulong)) - { - return _xid8; - } - } - } - - return 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 override RelationalTypeMapping? FindCollectionMapping( - RelationalTypeMappingInfo info, - // 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) - => FindCollectionMapping(info.StoreTypeName, modelType, providerType, elementMapping); - - /// - /// 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] - public virtual RelationalTypeMapping? FindCollectionMapping( - string? storeType, - // 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? modelClrType, - Type? providerClrType, - CoreTypeMapping? elementMapping) - { - if (elementMapping is not null and not RelationalTypeMapping) - { - return null; - } - - Type concreteCollectionType; - Type? elementType = null; - - if (modelClrType is not null) - { - // We do GetElementType for multidimensional arrays - these don't implement generic IEnumerable<> - elementType = modelClrType.TryGetElementType(typeof(IEnumerable<>)) ?? modelClrType.GetElementType(); - - // E.g. Newtonsoft.Json's JToken is enumerable over itself, exclude that scenario to avoid stack overflow. - if (elementType is null || elementType == modelClrType || modelClrType.GetGenericTypeImplementations(typeof(IDictionary<,>)).Any()) - { - return null; - } - } - - switch (storeType) - { - case null: - { - if (modelClrType is null) - { - return null; - } - - // If no mapping was found for the element CLR type, there's no mapping for the array. - // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL - Check.DebugAssert(elementType is not null, "elementClrType is null"); - - var relationalElementMapping = elementMapping as RelationalTypeMapping ?? FindMapping(elementType); - if (relationalElementMapping is not { ElementTypeMapping: null }) - { - return null; - } - - // If the element type mapping is a range, default to return a multirange type mapping (if the PG version supports it). - // Otherwise an array over the range will be returned. - if (_supportsMultiranges) - { - if (relationalElementMapping is NpgsqlRangeTypeMapping rangeMapping) - { - var multirangeStoreType = rangeMapping.StoreType switch - { - "int4range" => "int4multirange", - "int8range" => "int8multirange", - "numrange" => "nummultirange", - "tsrange" => "tsmultirange", - "tstzrange" => "tstzmultirange", - "daterange" => "datemultirange", - - _ => throw new InvalidOperationException( - $"Cannot create multirange type mapping for range type '{rangeMapping.StoreType}'") - }; - - return new NpgsqlMultirangeTypeMapping(multirangeStoreType, modelClrType, rangeMapping); - } - - // TODO: This needs to move to the NodaTime plugin, but there's no FindCollectionMapping extension yet for plugins - if (relationalElementMapping.GetType() is - { Name: "IntervalRangeMapping", Namespace: "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type1) - { - return (RelationalTypeMapping)Activator.CreateInstance( - type1.Assembly.GetType( - "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.IntervalMultirangeMapping")!, - modelClrType, - relationalElementMapping)!; - } - - if (relationalElementMapping.GetType() is - { Name: "DateIntervalRangeMapping", Namespace: "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type2) - { - return (RelationalTypeMapping)Activator.CreateInstance( - type2.Assembly.GetType( - "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.DateIntervalMultirangeMapping")!, - modelClrType, - relationalElementMapping)!; - } - } - - // Not a multirange - map as a PG array type - concreteCollectionType = FindTypeToInstantiate(modelClrType, elementType); - - return (NpgsqlArrayTypeMapping)Activator.CreateInstance( - typeof(NpgsqlArrayTypeMapping<,,>).MakeGenericType(modelClrType, concreteCollectionType, elementType), - relationalElementMapping)!; - } - - case var _ when storeType.EndsWith("[]", StringComparison.Ordinal): - { - // We have an array store type (either because we're reverse engineering or the user explicitly specified it) - var elementStoreType = storeType.Substring(0, storeType.Length - 2); - - // Note that we ignore the elementMapping argument here (but not in the CLR type-only path above). - // This is because the user-provided storeType for the array should take precedence over the element type mapping that gets - // calculated purely based on the element's CLR type in base.FindMappingWithConversion. - var relationalElementMapping = elementMapping as RelationalTypeMapping - ?? (elementType is null - ? FindMapping(elementStoreType) - : FindMapping(elementType, elementStoreType)); - if (relationalElementMapping is not { ElementTypeMapping: null }) - { - return null; - } - - // If no mapping was found for the element, there's no mapping for the array. - // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL - if (relationalElementMapping is not null and not NpgsqlArrayTypeMapping) - { - if (modelClrType is null) - { - // There's no model type - we're scaffolding purely from the store type. - elementType = relationalElementMapping.ClrType; - modelClrType = concreteCollectionType = typeof(List<>).MakeGenericType(elementType); - } - else - { - concreteCollectionType = FindTypeToInstantiate(modelClrType, elementType!); - Check.DebugAssert(elementType is not null, "elementType is null"); - } - - return (NpgsqlArrayTypeMapping)Activator.CreateInstance( - typeof(NpgsqlArrayTypeMapping<,,>).MakeGenericType(modelClrType, concreteCollectionType, elementType), - storeType, relationalElementMapping)!; - } - - return null; - } - - case var _ when IsMultirange(storeType, out var rangeStoreType) && _supportsMultiranges: - { - // Note that we ignore the elementMapping argument here (but not in the CLR type-only path above). - // This is because the user-provided storeType for the array should take precedence over the element type mapping that gets - // calculated purely based on the element's CLR type in base.FindMappingWithConversion. - var relationalElementMapping = elementType is null - ? FindMapping(rangeStoreType) - : FindMapping(elementType, rangeStoreType); - - // If no mapping was found for the element, there's no mapping for the array. - // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL - if (relationalElementMapping is NpgsqlRangeTypeMapping rangeMapping - // TODO: Why exclude if there's an element converter?? - && (relationalElementMapping.Converter is null || modelClrType is null || modelClrType.IsArrayOrGenericList())) - { - return new NpgsqlMultirangeTypeMapping( - storeType, modelClrType ?? typeof(List<>).MakeGenericType(relationalElementMapping.ClrType), rangeMapping); - } - - // TODO: This needs to move to the NodaTime plugin, but there's no FindCollectionMapping extension yet for plugins - if (relationalElementMapping?.GetType() is - { Name: "IntervalRangeMapping", Namespace: "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type1) - { - return (RelationalTypeMapping)Activator.CreateInstance( - type1.Assembly.GetType( - "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.IntervalMultirangeMapping")!, - modelClrType ?? relationalElementMapping.ClrType.MakeArrayType(), - relationalElementMapping)!; - } - - if (relationalElementMapping?.GetType() is - { Name: "DateIntervalRangeMapping", Namespace: "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal" } type2) - { - return (RelationalTypeMapping)Activator.CreateInstance( - type2.Assembly.GetType( - "Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.DateIntervalMultirangeMapping")!, - modelClrType ?? relationalElementMapping.ClrType.MakeArrayType(), - relationalElementMapping)!; - } - - return null; - } - -#pragma warning disable EF1001 // SelectExpression constructors are pubternal - // This is a scalar collection mapped to JSON, which - unlike with other relational providers - is not the default: in PG we - // map to PG arrays instead. - // However, when scalar collections are nested inside a JSON document, they obviously have to be JSON-mapped. - // In addition, at least in theory users may decide to map to JSON instead of array even outside JSON documents. - case "jsonb" or "json" when modelClrType is not null: - var mapping = base.FindCollectionMapping( - new RelationalTypeMappingInfo( - modelClrType, (RelationalTypeMapping?)elementMapping, storeTypeName: storeType, storeTypeNameBase: storeType), - modelClrType, - providerClrType, - elementMapping); - - // If a multidimensional array is mapped to json/jsonb, we get here, and base.FindCollectionMapping happily returns a - // "primitive" collection whose element mapping is an array; but EF does not support nested primitive collections. - // So we return null to let the calling code in FindBaseMapping simply return an NpgsqlJsonTypeMapping with no - // ElementTypeMapping - this case isn't at all handled as an EF primitive collection, but as an opaque JSON type. - return mapping is { ElementTypeMapping: NpgsqlArrayTypeMapping } ? null : mapping; -#pragma warning restore EF1001 // SelectExpression constructors are pubternal - - default: - return null; - } - - static bool IsMultirange(string multiRangeStoreType, [NotNullWhen(true)] out string? rangeStoreType) - { - rangeStoreType = multiRangeStoreType switch - { - "int4multirange" => "int4range", - "int8multirange" => "int8range", - "nummultirange" => "numrange", - "tsmultirange" => "tsrange", - "tstzmultirange" => "tstzrange", - "datemultirange" => "daterange", - _ => null - }; - - return rangeStoreType is not null; - } - - static Type FindTypeToInstantiate(Type collectionType, Type elementType) - { - if (collectionType.IsArray) - { - return collectionType; - } - - var listOfT = typeof(List<>).MakeGenericType(elementType); - - if (collectionType.IsAssignableFrom(listOfT)) - { - if (!collectionType.IsAbstract) - { - var constructor = collectionType.GetDeclaredConstructor(null); - if (constructor?.IsPublic == true) - { - return collectionType; - } - } - - return listOfT; - } - - return collectionType; - } - } - - /// - /// 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? FindRowValueMapping(in RelationalTypeMappingInfo mappingInfo) - => mappingInfo.ClrType is { } clrType - && clrType.IsAssignableTo(typeof(ITuple)) - ? 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 - /// 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? FindUserRangeMapping(in RelationalTypeMappingInfo mappingInfo) - { - UserRangeDefinition? rangeDefinition = null; - var rangeStoreType = mappingInfo.StoreTypeName; - var rangeClrType = mappingInfo.ClrType; - - // If the incoming MappingInfo contains a ClrType, make sure it's an NpgsqlRange, otherwise bail - if (rangeClrType is not null && (!rangeClrType.IsGenericType || rangeClrType.GetGenericTypeDefinition() != typeof(NpgsqlRange<>))) - { - return null; - } - - // Try to find a user range definition (defined by the user on their context options), based on the - // incoming MappingInfo's StoreType or ClrType - if (rangeStoreType is not null) - { - rangeDefinition = _userRangeDefinitions.SingleOrDefault(m => m.StoreTypeName == rangeStoreType); - - if (rangeDefinition is null) - { - return null; - } - - if (rangeClrType is null) - { - // The incoming MappingInfo does not contain a ClrType, only a StoreType (i.e. scaffolding). - // Construct the range ClrType from the range definition's subtype ClrType - rangeClrType = typeof(NpgsqlRange<>).MakeGenericType(rangeDefinition.SubtypeClrType); - } - else if (rangeClrType != typeof(NpgsqlRange<>).MakeGenericType(rangeDefinition.SubtypeClrType)) - { - // If the incoming MappingInfo also contains a ClrType (in addition to the StoreType), make sure it - // corresponds to the subtype ClrType on the range definition - return null; - } - } - else if (rangeClrType is not null) - { - rangeDefinition = _userRangeDefinitions.SingleOrDefault(m => m.SubtypeClrType == rangeClrType.GetGenericArguments()[0]); - } - - if (rangeClrType is null || rangeDefinition is null) - { - return null; - } - - // We now have a user-defined range definition from the context options. Use it to get the subtype's mapping - var subtypeMapping = rangeDefinition.SubtypeName is null - ? FindMapping(rangeDefinition.SubtypeClrType) - : FindMapping(rangeDefinition.SubtypeName); - - if (subtypeMapping is null) - { - 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.StoreTypeName, rangeDefinition.StoreTypeSchema); - var unquotedRangeStoreType = rangeDefinition.StoreTypeSchema is null - ? rangeDefinition.StoreTypeName - : rangeDefinition.StoreTypeSchema + '.' + rangeDefinition.StoreTypeName; - - return NpgsqlRangeTypeMapping.CreatUserDefinedRangeMapping( - quotedRangeStoreType, unquotedRangeStoreType, rangeClrType, subtypeMapping); - } - - /// - /// Finds the mapping for a container given its CLR type and its containee's type mapping; this is used when inferring type mappings - /// for arrays and ranges/multiranges. - /// - public virtual RelationalTypeMapping? FindContainerMapping( - Type containerClrType, - RelationalTypeMapping containeeTypeMapping, - IModel model) - { - // Ranges aren't handled by the general FindMapping logic below, as we don't represent range type mappings as having an element - // (they're not queryable). - if (containerClrType.IsRange()) - { - var rangeStoreType = containeeTypeMapping.StoreType switch - { - "int" or "integer" => "int4range", - "bigint" => "int8range", - "decimal" or "numeric" => "numrange", - "date" => "daterange", - "timestamp" or "timestamp without time zone" => "tsrange", - "timestamptz" or "timestamp with time zone" => "tstzrange", - _ => null - }; - - 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. - if (FindMapping(containerClrType, model, containeeTypeMapping) is RelationalTypeMapping containerMapping) - { - return containerMapping; - } - - return null; - } - - /// - public override RelationalTypeMapping? FindMapping(IProperty property) - { - var typeMapping = (RelationalTypeMapping?)base.FindMapping(property); - - if (typeMapping is NpgsqlArrayTypeMapping arrayMapping - && property.DeclaringType.IsMappedToJson()) - { - return FindCollectionMapping( - // TODO: Unfortunately GetContainerColumnType seems to return null at this point - property.DeclaringType.GetContainerColumnType() ?? "jsonb", - property.ClrType, - property.GetProviderClrType(), - arrayMapping.ElementTypeMapping); - } - - return typeMapping; - } - - private static bool NameBasesUsesPrecision(ReadOnlySpan span) - => span.ToString() switch - { - "decimal" => true, - "dec" => true, - "numeric" => true, - "timestamp" => true, - "timestamptz" => true, - "time" => true, - "interval" => true, - _ => false - }; - - /// - /// 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. - /// - // We override to support parsing array store names (e.g. varchar(32)[]), timestamp(5) with time zone, etc. - protected override string? ParseStoreTypeName( - string? storeTypeName, - ref bool? unicode, - ref int? size, - ref int? precision, - ref int? scale) - { - // TODO: Reimplement over ParseStoreTypeName below - - if (storeTypeName is null) - { - return null; - } - - var s = storeTypeName.AsSpan().Trim(); - - // 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). - if (s.EndsWith("[]", StringComparison.Ordinal)) - { - return storeTypeName; - } - - var openParen = s.IndexOf("(", StringComparison.Ordinal); - if (openParen == -1) - { - return storeTypeName; - } - - var preParens = s[..openParen].Trim(); - s = s.Slice(openParen + 1); - var closeParen = s.IndexOf(")", StringComparison.Ordinal); - if (closeParen == -1) - { - return storeTypeName; - } - - 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 storeTypeName; - } - - if (NameBasesUsesPrecision(preParens)) - { - precision = p; - scale = 0; - } - else - { - size = p; - } - - break; - - case var comma: - if (int.TryParse(s[..comma].Trim(), out var parsedPrecision)) - { - precision = parsedPrecision; - } - else - { - return storeTypeName; - } - - if (int.TryParse(s[(comma + 1)..closeParen].Trim(), out var parsedScale)) - { - scale = parsedScale; - } - else - { - return storeTypeName; - } - - break; - } - - if (postParens.Length == 0) - { - return preParens.Length == storeTypeName.Length - ? storeTypeName - : preParens.ToString(); - } - - 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 deleted file mode 100644 index 4d8fe64771..0000000000 --- a/src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System.Collections; -using static System.Linq.Expressions.Expression; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.ValueConversion; - -/// -/// A value converter that can convert between array types; accepts an optional for the element, but can be -/// used without one to convert e.g. from a list to an array. -/// -public class NpgsqlArrayConverter - : ValueConverter - where TModelCollection : IEnumerable - where TConcreteModelCollection : IEnumerable - where TProviderCollection : IEnumerable -{ - /// - /// The value converter for the element type of the array. - /// - public virtual ValueConverter? ElementConverter { get; } - - /// - /// Constructs a new instance of . - /// - public NpgsqlArrayConverter() - : this(elementConverter: null) - { - } - - /// - /// Constructs a new instance of . - /// - public NpgsqlArrayConverter(ValueConverter? elementConverter) - : base( - // We assume that TProviderCollection is always a concrete, instantiable type (in fact it's always an array over the element) - ArrayConversionExpression( - elementConverter?.ConvertToProviderExpression), - ArrayConversionExpression( - elementConverter?.ConvertFromProviderExpression)) - { - var modelElementType = typeof(TModelCollection).TryGetElementType(typeof(IEnumerable<>)); - var providerElementType = typeof(TProviderCollection).TryGetElementType(typeof(IEnumerable<>)); - if (modelElementType is null || providerElementType is null) - { - throw new ArgumentException("Can only convert between arrays"); - } - - if (elementConverter is not null) - { - if (modelElementType.UnwrapNullableType() != elementConverter.ModelClrType.UnwrapNullableType()) - { - throw new ArgumentException( - $"The element's value converter model type ({elementConverter.ModelClrType}), doesn't match the array's ({modelElementType})"); - } - - if (providerElementType.UnwrapNullableType() != elementConverter.ProviderClrType.UnwrapNullableType()) - { - throw new ArgumentException( - $"The element's value converter provider type ({elementConverter.ProviderClrType}), doesn't match the array's ({providerElementType})"); - } - } - - ElementConverter = elementConverter; - } - - /// - /// Generates a lambda expression that accepts an array, and converts it to another array by looping and applying - /// a conversion lambda to each of its elements. - /// - private static Expression> ArrayConversionExpression( - LambdaExpression? elementConversionExpression) - { - var inputElementType = typeof(TInput).IsArray - ? typeof(TInput).GetElementType() - : typeof(TInput).TryGetElementType(typeof(IEnumerable<>)); - - var outputElementType = typeof(TOutput).IsArray - ? typeof(TOutput).GetElementType() - : typeof(TOutput).TryGetElementType(typeof(IEnumerable<>)); - - if (inputElementType is null || outputElementType is null) - { - throw new ArgumentException("Both TInput and TOutput must be arrays or IList"); - } - - // elementConversionExpression is always over non-nullable value types. If the array is over nullable types, - // we need to sanitize via an external null check. - if (elementConversionExpression is not null && inputElementType.IsNullableType() && outputElementType.IsNullableType()) - { - // p => p is null ? null : elementConversionExpression(p) - var p = Parameter(inputElementType, "foo"); - elementConversionExpression = Lambda( - Condition( - Equal(p, Constant(null, inputElementType)), - Constant(null, outputElementType), - Convert( - Invoke( - elementConversionExpression, - // The user-provided conversion lambda typically accepts non-nullable (value) types, with EF Core doing the - // null-sanitization and conversion to non-nullable; do this here unless the user-provided lambda happens to - // accept a nullable value type parameter. - elementConversionExpression.Parameters[0].Type.IsNullableType() - ? p - : Convert(p, inputElementType.UnwrapNullableType())), - outputElementType)), - p); - } - - var input = Parameter(typeof(TInput), "input"); - var convertedInput = input; - var output = Parameter(typeof(TConcreteOutput), "result"); - var lengthVariable = Variable(typeof(int), "length"); - - var expressions = new List(); - var variables = new List { output, lengthVariable }; - - Expression getInputLength; - Func? indexer; - - // 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 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; - - // 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, - input.Type.GetProperty("Count") - // If TInput is an interface (ICollection), its Count property needs to be found on ICollection - ?? typeof(ICollection<>).MakeGenericType(input.Type.GetGenericArguments()[0]).GetProperty("Count")!); - indexer = null; - break; - } - - // 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)}"); - } - - Expression? instantiateOutput = typeof(TConcreteOutput) switch - { - var t when t.IsArray => NewArrayBounds(outputElementType, lengthVariable), - var t when typeof(TConcreteOutput).GetConstructor([typeof(int)]) is ConstructorInfo ctorWithLength => New(ctorWithLength, lengthVariable), - var t when typeof(TConcreteOutput).GetConstructor([]) is not null => New(typeof(TConcreteOutput)), - - _ => null - }; - - if (instantiateOutput is null) - { - // If the output type can't be instantiated (no public parameterless constructor), we can't value convert to it. - // If we simply throw and prevent the array converter from being instantiated, that would block scenarios where the user is - // only *writing* an uninstantiable type, but never reading it (see #3050). To allow for such "one-directional" value converters, - // we instead return a lambda that throws, so that if converter is actually ever used to read, it will throw at that point. - // See test Parameter_collection_Dictionary_Valuees_with_value_converter_Contains. - return Lambda>( - Throw( - New( - typeof(InvalidOperationException).GetConstructor([typeof(string)])!, - Constant($"Type {typeof(TConcreteOutput)} cannot be instantiated as it does not have a public parameterless constructor.")), - typeof(TOutput)), - input); - } - - expressions.AddRange( - [ - // 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, instantiateOutput) - ]); - - 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] = convert(input[i]); - // } - var counter = Parameter(typeof(int), "i"); - - expressions.Add( - ForLoop( - loopVar: counter, - initValue: Constant(0), - condition: LessThan(counter, lengthVariable), - increment: AddAssign(counter, Constant(1)), - loopContent: - typeof(TConcreteOutput).IsArray - ? Assign( - ArrayAccess(output, counter), - elementConversionExpression is null - ? indexer(counter) - : Invoke(elementConversionExpression, indexer(counter))) - : Call( - output, - typeof(TConcreteOutput).GetMethod("Add", [outputElementType])!, - elementConversionExpression is null - ? 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] = convert(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); - - Expression body = Block(typeof(TOutput), variables, expressions); - - // If the input type is a reference type, first check if the input array is null and return null - // (or default for output value type) if so, bypassing all of the logic above. - if (!typeof(TInput).IsValueType) - { - body = Condition( - ReferenceEqual(input, Constant(null, typeof(TInput))), - typeof(TOutput).IsValueType - ? New(typeof(TConcreteOutput)) - : Constant(null, typeof(TOutput)), - body); - } - - return Lambda>(body, input); - } - - private static Expression ForLoop( - ParameterExpression loopVar, - Expression initValue, - Expression condition, - Expression increment, - Expression loopContent) - { - var initAssign = Assign(loopVar, initValue); - var breakLabel = Label("LoopBreak"); - var loop = Block( - [loopVar], - initAssign, - Loop( - IfThenElse( - condition, - Block( - loopContent, - increment - ), - Break(breakLabel) - ), - breakLabel) - ); - - return loop; - } -} diff --git a/src/EFCore.PG/Types/NpgsqlTsRankingNormalization.cs b/src/EFCore.PG/Types/NpgsqlTsRankingNormalization.cs deleted file mode 100644 index ee254b152f..0000000000 --- a/src/EFCore.PG/Types/NpgsqlTsRankingNormalization.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore; - -/// -/// Specifies whether and how a document's length should impact its rank. -/// This is used with the ranking functions in . -/// See http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-RANKING -/// for more information about the behaviors that are controlled by this value. -/// -[Flags] -[SuppressMessage("ReSharper", "UnusedMember.Global")] -public enum NpgsqlTsRankingNormalization -{ - /// - /// Ignores the document length. - /// - Default = 0, - - /// - /// Divides the rank by 1 + the logarithm of the document length. - /// - DivideBy1PlusLogLength = 1, - - /// - /// Divides the rank by the document length. - /// - DivideByLength = 2, - - /// - /// Divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd). - /// - DivideByMeanHarmonicDistanceBetweenExtents = 4, - - /// - /// Divides the rank by the number of unique words in document. - /// - DivideByUniqueWordCount = 8, - - /// - /// Divides the rank by 1 + the logarithm of the number of unique words in document. - /// - DividesBy1PlusLogUniqueWordCount = 16, - - /// - /// Divides the rank by itself + 1. - /// - DivideByItselfPlusOne = 32 -} diff --git a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommand.cs b/src/EFCore.PG/Update/Internal/NpgsqlModificationCommand.cs deleted file mode 100644 index 87f7dd01ad..0000000000 --- a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommand.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Data; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update.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 NpgsqlModificationCommand : ModificationCommand -{ - private readonly bool _detailedErrorsEnabled; - - /// - /// 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 NpgsqlModificationCommand(in ModificationCommandParameters modificationCommandParameters) - : base(in modificationCommandParameters) - { - _detailedErrorsEnabled = modificationCommandParameters.DetailedErrorsEnabled; - } - - /// - /// 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 NpgsqlModificationCommand(in NonTrackedModificationCommandParameters modificationCommandParameters) - : base(in modificationCommandParameters) - { - } - - /// - /// 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 void ProcessSinglePropertyJsonUpdate(ref ColumnModificationParameters parameters) - { - // PG jsonb_set accepts a jsonb parameter for the value to be set - not an int, boolean or string like many other providers. - // So we always pass the value through the mapping's ToJsonString() (except for null). - var mapping = parameters.Property!.GetRelationalTypeMapping(); - var value = parameters.Value; - - value = value is null - ? "null" - : (mapping.JsonValueReaderWriter?.ToJsonString(value) - ?? (mapping.Converter == null ? value : mapping.Converter.ConvertToProvider(value))); - - parameters = parameters with { Value = value }; - } - - /// - /// 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 PropagateResults(RelationalDataReader relationalReader) - { - // The default implementation of PropagateResults skips (output) parameters, since for e.g. SQL Server these aren't yet populated - // when consuming the result set (propagating output columns is done later, after the reader is closed). - // However, in PostgreSQL, output parameters actually get returned as the result set, so we override and take care of that here. - var columnCount = ColumnModifications.Count; - - var readerIndex = -1; - - for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) - { - var columnModification = ColumnModifications[columnIndex]; - - switch (columnModification.Column) - { - case IColumn when columnModification.IsRead: - case IStoreStoredProcedureParameter { Direction: ParameterDirection.Output or ParameterDirection.InputOutput }: - readerIndex++; - break; - - case IColumn: - case IStoreStoredProcedureParameter: - case null when columnModification.JsonPath is not null: - continue; - - default: - throw new ArgumentOutOfRangeException(); - } - - // For regular result sets, results are always propagated back into entity properties. - // But with stored procedures, there may be a rows affected result column (generated by an output parameter definition). - // Skip these. - if (columnModification.Property is null || !columnModification.IsRead) - { - continue; - } - - columnModification.Value = - columnModification.Property.GetReaderFieldValue(relationalReader, readerIndex, _detailedErrorsEnabled); - } - } -} diff --git a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs b/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs deleted file mode 100644 index 2bc592ff9a..0000000000 --- a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatch.cs +++ /dev/null @@ -1,300 +0,0 @@ -using System.Data; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal; - -/// -/// The Npgsql-specific implementation for . -/// -/// -/// The usual ModificationCommandBatch implementation is , -/// which selects the number of rows modified via a SQL query. -/// PostgreSQL actually has no way of selecting the modified row count. -/// SQL defines GET DIAGNOSTICS which should provide this, but in PostgreSQL it's only available -/// in PL/pgSQL. See http://www.postgresql.org/docs/9.4/static/unsupported-features-sql-standard.html, -/// identifier F121-01. -/// Instead, the affected row count can be accessed in the PostgreSQL protocol itself, which seems -/// cleaner and more efficient anyway (no additional query). -/// -public class NpgsqlModificationCommandBatch : ReaderModificationCommandBatch -{ - /// - /// Constructs an instance of the class. - /// - public NpgsqlModificationCommandBatch( - ModificationCommandBatchFactoryDependencies dependencies, - int maxBatchSize) - : base(dependencies) - { - MaxBatchSize = maxBatchSize; - } - - /// - /// The maximum number of instances that can be added to a single batch; defaults to 1000. - /// - protected override int MaxBatchSize { 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. - /// - protected override void AddParameter(IColumnModification columnModification) - { - // PostgreSQL stored procedures cannot return a regular result set, and output parameter values are simply sent back as the - // result set; this is very different from SQL Server, where output parameter values can be sent back in addition to result - // sets. So we avoid adding NpgsqlParameters for output parameters - we'll just retrieve and propagate the values below when - // consuming the result set. - if (columnModification.Column is IStoreStoredProcedureParameter { Direction: ParameterDirection.Output }) - { - return; - } - - base.AddParameter(columnModification); - } - - /// - /// 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 void Consume(RelationalDataReader reader) - => Consume(reader, async: false).GetAwaiter().GetResult(); - - /// - /// 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 Task ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken = default) - => Consume(reader, async: true, cancellationToken); - - private async Task Consume(RelationalDataReader reader, bool async, CancellationToken cancellationToken = default) - { - var npgsqlReader = (NpgsqlDataReader)reader.DbDataReader; - -#pragma warning disable 618 - Debug.Assert( - npgsqlReader.Statements.Count == ModificationCommands.Count, - $"Reader has {npgsqlReader.Statements.Count} statements, expected {ModificationCommands.Count}"); -#pragma warning restore 618 - - var commandIndex = 0; - - try - { - bool? onResultSet = null; - while (commandIndex < ModificationCommands.Count) - { - var command = ModificationCommands[commandIndex]; - - // Note that in the PG provider, we never transmit rows affected via the result set (except with stored procedures); - // it's transmitted separately via the PG wire protocol and exposed on the reader (see below). - // As a result, if there's a result set we know that it contains values to be propagated back into the entity instance. - if (ResultSetMappings[commandIndex].HasFlag(ResultSetMapping.HasResultRow)) - { - if (async) - { - if (!(await reader.ReadAsync(cancellationToken).ConfigureAwait(false))) - { - await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, cancellationToken) - .ConfigureAwait(false); - } - } - else - { - if (!reader.Read()) - { - ThrowAggregateUpdateConcurrencyException(reader, commandIndex, 1, 0); - } - } - - if (command.RowsAffectedColumn is { } rowsAffectedColumn) - { - // Only stored procedures have rows affected, as an output parameter. For regular commands, the rows affected gets - // transferred via the Post - Debug.Assert(command.StoreStoredProcedure is not null); - - var rowsAffectedParameter = (IStoreStoredProcedureParameter)rowsAffectedColumn; - Debug.Assert(rowsAffectedParameter.Direction == ParameterDirection.Output); - - var readerIndex = -1; - - for (var i = 0; i < command.ColumnModifications.Count; i++) - { - var columnModification = command.ColumnModifications[i]; - if (columnModification.Column is IStoreStoredProcedureParameter - { - Direction: ParameterDirection.Output or ParameterDirection.InputOutput - }) - { - readerIndex++; - } - - if (columnModification.Column == rowsAffectedColumn) - { - break; - } - } - - if (reader.DbDataReader.GetInt32(readerIndex) != 1) - { - await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, cancellationToken) - .ConfigureAwait(false); - } - } - - command.PropagateResults(reader); - - commandIndex++; - - onResultSet = async - ? await npgsqlReader.NextResultAsync(cancellationToken).ConfigureAwait(false) - : npgsqlReader.NextResult(); - } - else - { - Debug.Assert(ResultSetMappings[commandIndex] == ResultSetMapping.NoResults); - - // With stored procedures, either the rows affected is returned via an output parameter (and is therefore a result - // column and handled above), or there's no rows affected and we skip the check entirely. - // Without stored procedures, PG transmits the rows affected for each statement in the CommandComplete message of the - // protocol, and we always just check that. - // TODO: when EF Core adds support for DbBatch (https://github.com/dotnet/efcore/issues/18990), we can start using that - // standardized API for fetching the rows affected by an individual command in a batch. -#pragma warning disable 618 - if (npgsqlReader.Statements[commandIndex].Rows != 1 && command.StoreStoredProcedure is null) - { - if (async) - { - await ThrowAggregateUpdateConcurrencyExceptionAsync(reader, commandIndex, 1, 0, cancellationToken) - .ConfigureAwait(false); - } - else - { - ThrowAggregateUpdateConcurrencyException(reader, commandIndex, 1, 0); - } - } -#pragma warning restore 618 - commandIndex++; - } - } - - if (onResultSet == true) - { - Dependencies.UpdateLogger.UnexpectedTrailingResultSetWhenSaving(); - } - } - 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, - ModificationCommands[commandIndex].Entries); - } - } - - private IReadOnlyList AggregateEntries(int endIndex, int commandCount) - { - var entries = new List(); - for (var i = endIndex - commandCount; i < endIndex; i++) - { - entries.AddRange(ModificationCommands[i].Entries); - } - - return entries; - } - - /// - /// Throws an exception indicating the command affected an unexpected number of rows. - /// - /// The data reader. - /// The ordinal of the command. - /// The expected number of rows affected. - /// The actual number of rows affected. - protected virtual void ThrowAggregateUpdateConcurrencyException( - RelationalDataReader reader, - int commandIndex, - int expectedRowsAffected, - int rowsAffected) - { - var entries = AggregateEntries(commandIndex + 1, expectedRowsAffected); - var exception = new DbUpdateConcurrencyException( - RelationalStrings.UpdateConcurrencyException(expectedRowsAffected, rowsAffected), - entries); - - if (!Dependencies.UpdateLogger.OptimisticConcurrencyException( - Dependencies.CurrentContext.Context, - entries, - exception, - (c, ex, e, d) => CreateConcurrencyExceptionEventData(c, reader, ex, e, d)).IsSuppressed) - { - throw exception; - } - } - - /// - /// Throws an exception indicating the command affected an unexpected number of rows. - /// - /// The data reader. - /// The ordinal of the command. - /// The expected number of rows affected. - /// The actual number of rows affected. - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. - /// If the is canceled. - protected virtual async Task ThrowAggregateUpdateConcurrencyExceptionAsync( - RelationalDataReader reader, - int commandIndex, - int expectedRowsAffected, - int rowsAffected, - CancellationToken cancellationToken) - { - var entries = AggregateEntries(commandIndex + 1, expectedRowsAffected); - var exception = new DbUpdateConcurrencyException( - RelationalStrings.UpdateConcurrencyException(expectedRowsAffected, rowsAffected), - entries); - - if (!(await Dependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync( - Dependencies.CurrentContext.Context, - entries, - exception, - (c, ex, e, d) => CreateConcurrencyExceptionEventData(c, reader, ex, e, d), - cancellationToken: cancellationToken) - .ConfigureAwait(false)).IsSuppressed) - { - throw exception; - } - } - - private static RelationalConcurrencyExceptionEventData CreateConcurrencyExceptionEventData( - DbContext context, - RelationalDataReader reader, - DbUpdateConcurrencyException exception, - IReadOnlyList entries, - EventDefinition definition) - => new( - definition, - (definition1, payload) - => ((EventDefinition)definition1).GenerateMessage(((ConcurrencyExceptionEventData)payload).Exception), - context, - reader.RelationalConnection.DbConnection, - reader.DbCommand, - reader.DbDataReader, - reader.CommandId, - reader.RelationalConnection.ConnectionId, - entries, - exception); -} diff --git a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatchFactory.cs b/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatchFactory.cs deleted file mode 100644 index 843b203b66..0000000000 --- a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandBatchFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update.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 NpgsqlModificationCommandBatchFactory : IModificationCommandBatchFactory -{ - private const int DefaultMaxBatchSize = 1000; - - private readonly ModificationCommandBatchFactoryDependencies _dependencies; - private readonly int _maxBatchSize; - - /// - /// 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 NpgsqlModificationCommandBatchFactory(ModificationCommandBatchFactoryDependencies dependencies, IDbContextOptions options) - { - Check.NotNull(dependencies, nameof(dependencies)); - Check.NotNull(options, nameof(options)); - - _dependencies = dependencies; - - _maxBatchSize = options.FindExtension()?.MaxBatchSize ?? DefaultMaxBatchSize; - - if (_maxBatchSize <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(RelationalOptionsExtension.MaxBatchSize), RelationalStrings.InvalidMaxBatchSize(_maxBatchSize)); - } - } - - /// - /// 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 ModificationCommandBatch Create() - => new NpgsqlModificationCommandBatch(_dependencies, _maxBatchSize); -} diff --git a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandFactory.cs b/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandFactory.cs deleted file mode 100644 index 7b5cc4dc97..0000000000 --- a/src/EFCore.PG/Update/Internal/NpgsqlModificationCommandFactory.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update.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 NpgsqlModificationCommandFactory : IModificationCommandFactory -{ - /// - /// 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 IModificationCommand CreateModificationCommand( - in ModificationCommandParameters modificationCommandParameters) - => new NpgsqlModificationCommand(modificationCommandParameters); - - /// - /// 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 INonTrackedModificationCommand CreateNonTrackedModificationCommand( - in NonTrackedModificationCommandParameters modificationCommandParameters) - => new NpgsqlModificationCommand(modificationCommandParameters); -} diff --git a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs b/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs deleted file mode 100644 index cecf95aee3..0000000000 --- a/src/EFCore.PG/Update/Internal/NpgsqlUpdateSqlGenerator.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System.Data; -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update.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 NpgsqlUpdateSqlGenerator : UpdateSqlGenerator -{ - /// - /// 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 NpgsqlUpdateSqlGenerator(UpdateSqlGeneratorDependencies dependencies) - : base(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 - /// 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 ResultSetMapping AppendInsertOperation( - StringBuilder commandStringBuilder, - IReadOnlyModificationCommand command, - int commandPosition, - out bool requiresTransaction) - => AppendInsertOperation(commandStringBuilder, command, commandPosition, overridingSystemValue: false, out requiresTransaction); - - /// - /// 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 ResultSetMapping AppendInsertOperation( - StringBuilder commandStringBuilder, - IReadOnlyModificationCommand command, - int commandPosition, - bool overridingSystemValue, - out bool requiresTransaction) - { - var name = command.TableName; - var schema = command.Schema; - var operations = command.ColumnModifications; - - var writeOperations = operations.Where(o => o.IsWrite).ToList(); - var readOperations = operations.Where(o => o.IsRead).ToList(); - - AppendInsertCommand(commandStringBuilder, name, schema, writeOperations, readOperations, overridingSystemValue); - - requiresTransaction = false; - - return readOperations.Count > 0 ? ResultSetMapping.LastInResultSet : ResultSetMapping.NoResults; - } - - /// - /// 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 void AppendInsertCommand( - StringBuilder commandStringBuilder, - string name, - string? schema, - IReadOnlyList writeOperations, - IReadOnlyList readOperations, - bool overridingSystemValue) - { - AppendInsertCommandHeader(commandStringBuilder, name, schema, writeOperations); - - if (overridingSystemValue) - { - commandStringBuilder.AppendLine().Append("OVERRIDING SYSTEM VALUE"); - } - - AppendValuesHeader(commandStringBuilder, writeOperations); - AppendValues(commandStringBuilder, name, schema, writeOperations); - AppendReturningClause(commandStringBuilder, readOperations); - commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); - } - - /// - /// 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 ResultSetMapping AppendUpdateOperation( - StringBuilder commandStringBuilder, - IReadOnlyModificationCommand command, - int commandPosition, - out bool requiresTransaction) - { - // The default implementation adds RETURNING 1 to do concurrency check (was the row actually updated), but in PostgreSQL we check - // the per-statement row-affected value exposed by Npgsql in the batch; so no need for RETURNING 1. - var name = command.TableName; - var schema = command.Schema; - var operations = command.ColumnModifications; - - var writeOperations = operations.Where(o => o.IsWrite).ToList(); - var conditionOperations = operations.Where(o => o.IsCondition).ToList(); - var readOperations = operations.Where(o => o.IsRead).ToList(); - - requiresTransaction = false; - - AppendUpdateCommand(commandStringBuilder, name, schema, writeOperations, readOperations, conditionOperations); - - return readOperations.Count > 0 ? ResultSetMapping.LastInResultSet : ResultSetMapping.NoResults; - } - - /// - protected override void AppendUpdateColumnValue( - ISqlGenerationHelper updateSqlGeneratorHelper, - IColumnModification columnModification, - StringBuilder stringBuilder, - string name, - string? schema) - { - if (columnModification.JsonPath is not (null or "$")) - { - Check.DebugAssert( - columnModification.TypeMapping is NpgsqlStructuralJsonTypeMapping, - "ColumnModification with JsonPath but non-NpgsqlOwnedJsonTypeMapping"); - - if (columnModification.TypeMapping.StoreType is "json") - { - throw new NotSupportedException( - "Cannot perform partial update because the PostgreSQL 'json' type has no json_set method. Use 'jsonb' instead."); - } - - Check.DebugAssert(columnModification.TypeMapping.StoreType is "jsonb", "Non-jsonb type mapping in JSON partial update"); - - // TODO: Lax or not? - stringBuilder - .Append("jsonb_set(") - .Append(updateSqlGeneratorHelper.DelimitIdentifier(columnModification.ColumnName)) - .Append(", '{"); - - // TODO: Unfortunately JsonPath is provided as a JSONPATH string, but PG's jsonb_set requires the path as an array. - // Parse the components back out (https://github.com/dotnet/efcore/issues/32185) - var components = columnModification.JsonPath.Split("."); - var needsComma = false; - for (var i = 0; i < components.Length; i++) - { - if (needsComma) - { - stringBuilder.Append(','); - } - - var component = components[i]; - var bracketOpen = component.IndexOf('['); - if (bracketOpen == -1) - { - if (i > 0) // The first component is $, representing the root - { - stringBuilder.Append(component); - needsComma = true; - } - - continue; - } - - var propertyName = component[..bracketOpen]; - if (i > 0) // The first component is $, representing the root - { - stringBuilder - .Append(propertyName) - .Append(','); - } - - stringBuilder.Append(component[(bracketOpen + 1)..^1]); - needsComma = true; - } - - stringBuilder.Append("}', "); - - // TODO: Hack around - if (columnModification.Value is null) - { - _columnModificationValueField ??= typeof(ColumnModification).GetField( - "_value", BindingFlags.Instance | BindingFlags.NonPublic)!; - _columnModificationValueField.SetValue(columnModification, "null"); - } - - base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); - - stringBuilder.Append(")"); - } - else - { - base.AppendUpdateColumnValue(updateSqlGeneratorHelper, columnModification, stringBuilder, name, schema); - } - } - - private FieldInfo? _columnModificationValueField; - - /// - /// 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 ResultSetMapping AppendDeleteOperation( - StringBuilder commandStringBuilder, - IReadOnlyModificationCommand command, - int commandPosition, - out bool requiresTransaction) - { - // The default implementation adds RETURNING 1 to do concurrency check (was the row actually deleted), but in PostgreSQL we check - // the per-statement row-affected value exposed by Npgsql in the batch; so no need for RETURNING 1. - var name = command.TableName; - var schema = command.Schema; - var conditionOperations = command.ColumnModifications.Where(o => o.IsCondition).ToList(); - - requiresTransaction = false; - - AppendDeleteCommand(commandStringBuilder, name, schema, [], conditionOperations); - - return ResultSetMapping.NoResults; - } - - /// - /// 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 ResultSetMapping AppendStoredProcedureCall( - StringBuilder commandStringBuilder, - IReadOnlyModificationCommand command, - int commandPosition, - out bool requiresTransaction) - { - Check.DebugAssert(command.StoreStoredProcedure is not null, "command.StoreStoredProcedure is not null"); - - var storedProcedure = command.StoreStoredProcedure; - - Check.DebugAssert( - storedProcedure.Parameters.Any() || storedProcedure.ResultColumns.Any(), - "Stored procedure call with neither parameters nor result columns"); - - var resultSetMapping = ResultSetMapping.NoResults; - - commandStringBuilder.Append("CALL "); - - // PostgreSQL supports neither a return value nor a result set with stored procedures, only output parameters. - Check.DebugAssert(storedProcedure.ReturnValue is null, "storedProcedure.Return is null"); - Check.DebugAssert(!storedProcedure.ResultColumns.Any(), "!storedProcedure.ResultColumns.Any()"); - - SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, storedProcedure.Name, storedProcedure.Schema); - - commandStringBuilder.Append('('); - - var first = true; - - // Only positional parameter style supported for now, see https://github.com/dotnet/efcore/issues/28439 - - // Note: the column modifications are already ordered according to the sproc parameter ordering - // (see ModificationCommand.GenerateColumnModifications) - for (var i = 0; i < command.ColumnModifications.Count; i++) - { - var columnModification = command.ColumnModifications[i]; - var parameter = (IStoreStoredProcedureParameter)columnModification.Column!; - - if (first) - { - first = false; - } - else - { - commandStringBuilder.Append(", "); - } - - Check.DebugAssert(columnModification.UseParameter, "Column modification matched a parameter, but UseParameter is false"); - - if (parameter.Direction == ParameterDirection.Output) - { - // Recommended PG practice is to pass NULL for output parameters - commandStringBuilder.Append("NULL"); - } - else - { - SqlGenerationHelper.GenerateParameterNamePlaceholder( - commandStringBuilder, columnModification.UseOriginalValueParameter - ? columnModification.OriginalParameterName! - : columnModification.ParameterName!); - } - - // PostgreSQL stored procedures cannot return a regular result set, and output parameter values are simply sent back as the - // result set; this is very different from SQL Server, where output parameter values can be sent back in addition to result - // sets. - if (parameter.Direction.HasFlag(ParameterDirection.Output)) - { - // The distinction between having only a rows affected output parameter and having other non-rows affected parameters - // is important later on (i.e. whether we need to propagate or not). - resultSetMapping = parameter == command.RowsAffectedColumn && resultSetMapping == ResultSetMapping.NoResults - ? ResultSetMapping.ResultSetWithRowsAffectedOnly - : ResultSetMapping.LastInResultSet; - } - } - - commandStringBuilder.Append(')'); - - commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); - - requiresTransaction = true; - - return resultSetMapping; - } - - /// - /// 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 AppendObtainNextSequenceValueOperation(StringBuilder commandStringBuilder, string name, string? schema) - { - commandStringBuilder.Append("nextval('"); - SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, Check.NotNull(name, nameof(name)), schema); - commandStringBuilder.Append("')"); - } -} diff --git a/src/EFCore.PG/Utilities/Util.cs b/src/EFCore.PG/Utilities/Util.cs deleted file mode 100644 index 97bca187c1..0000000000 --- a/src/EFCore.PG/Utilities/Util.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -internal static class Statics -{ - internal static readonly bool[][] TrueArrays = - [ - [], - [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 = [ - [], - [false], - [false, false], - [false, false, false] - ]; -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs b/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs deleted file mode 100644 index 51d91b739a..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/INpgsqlSequenceValueGeneratorFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -/// -/// 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 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? TryCreate( - IProperty property, - Type clrType, - NpgsqlSequenceValueGeneratorState generatorState, - INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder, - IRelationalCommandDiagnosticsLogger commandLogger); -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/INpgsqlValueGeneratorCache.cs b/src/EFCore.PG/ValueGeneration/Internal/INpgsqlValueGeneratorCache.cs deleted file mode 100644 index cd10933b14..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/INpgsqlValueGeneratorCache.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -/// -/// 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 interface INpgsqlValueGeneratorCache : IValueGeneratorCache -{ - /// - /// 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. - /// - NpgsqlSequenceValueGeneratorState GetOrAddSequenceState( - IProperty property, - IRelationalConnection connection); -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceHiLoValueGenerator.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceHiLoValueGenerator.cs deleted file mode 100644 index cf8483343f..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceHiLoValueGenerator.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Globalization; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -/// -/// 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 NpgsqlSequenceHiLoValueGenerator : HiLoValueGenerator -{ - private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder; - private readonly IUpdateSqlGenerator _sqlGenerator; - private readonly INpgsqlRelationalConnection _connection; - private readonly ISequence _sequence; - private readonly IRelationalCommandDiagnosticsLogger _commandLogger; - - /// - /// 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 NpgsqlSequenceHiLoValueGenerator( - IRawSqlCommandBuilder rawSqlCommandBuilder, - IUpdateSqlGenerator sqlGenerator, - NpgsqlSequenceValueGeneratorState generatorState, - INpgsqlRelationalConnection connection, - IRelationalCommandDiagnosticsLogger commandLogger) - : base(generatorState) - { - _sequence = generatorState.Sequence; - _rawSqlCommandBuilder = rawSqlCommandBuilder; - _sqlGenerator = sqlGenerator; - _connection = connection; - _commandLogger = commandLogger; - } - - /// - /// 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. - /// - protected override long GetNewLowValue() - => (long)Convert.ChangeType( - _rawSqlCommandBuilder - .Build(_sqlGenerator.GenerateNextSequenceValueOperation(_sequence.Name, _sequence.Schema)) - .ExecuteScalar( - new RelationalCommandParameterObject( - _connection, - parameterValues: null, - readerColumns: null, - context: null, - _commandLogger)), - typeof(long), - CultureInfo.InvariantCulture)!; - - /// - /// 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. - /// - protected override async Task GetNewLowValueAsync(CancellationToken cancellationToken = default) - => (long)Convert.ChangeType( - await _rawSqlCommandBuilder - .Build(_sqlGenerator.GenerateNextSequenceValueOperation(_sequence.Name, _sequence.Schema)) - .ExecuteScalarAsync( - new RelationalCommandParameterObject( - _connection, - parameterValues: null, - readerColumns: null, - context: null, - _commandLogger), - cancellationToken).ConfigureAwait(false), - typeof(long), - CultureInfo.InvariantCulture)!; - - /// - /// 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; -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs deleted file mode 100644 index 37bf81d936..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorFactory.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -/// -/// 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 NpgsqlSequenceValueGeneratorFactory : INpgsqlSequenceValueGeneratorFactory -{ - private readonly IUpdateSqlGenerator _sqlGenerator; - - /// - /// 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 NpgsqlSequenceValueGeneratorFactory( - IUpdateSqlGenerator sqlGenerator) - { - Check.NotNull(sqlGenerator, nameof(sqlGenerator)); - - _sqlGenerator = sqlGenerator; - } - - /// - /// 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? TryCreate( - IProperty property, - Type type, - NpgsqlSequenceValueGeneratorState generatorState, - INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder, - IRelationalCommandDiagnosticsLogger commandLogger) - { - if (type == typeof(long)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(int)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(short)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(byte)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(char)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(ulong)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(uint)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(ushort)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - if (type == typeof(sbyte)) - { - return new NpgsqlSequenceHiLoValueGenerator( - rawSqlCommandBuilder, _sqlGenerator, generatorState, connection, commandLogger); - } - - return null; - } -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorState.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorState.cs deleted file mode 100644 index ec4c8dce90..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlSequenceValueGeneratorState.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -/// -/// 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 NpgsqlSequenceValueGeneratorState : HiLoValueGeneratorState -{ - /// - /// 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 NpgsqlSequenceValueGeneratorState(ISequence sequence) - : base(Check.NotNull(sequence, nameof(sequence)).IncrementBy) - { - Sequence = sequence; - } - - /// - /// 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 ISequence Sequence { get; } -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs deleted file mode 100644 index 413988d165..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Concurrent; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal; - -/// -/// 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 NpgsqlValueGeneratorCache : ValueGeneratorCache, INpgsqlValueGeneratorCache -{ - private readonly ConcurrentDictionary _sequenceGeneratorCache - = new(); - - /// - /// Initializes a new instance of the class. - /// - /// Parameter object containing dependencies for this service. - public NpgsqlValueGeneratorCache(ValueGeneratorCacheDependencies dependencies) - : base(dependencies) - { - } - - /// - /// 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 NpgsqlSequenceValueGeneratorState GetOrAddSequenceState( - IProperty property, - IRelationalConnection connection) - { - var sequence = property.FindHiLoSequence(); - - Debug.Assert(sequence is not null); - - return _sequenceGeneratorCache.GetOrAdd( - GetSequenceName(sequence, connection), - _ => new NpgsqlSequenceValueGeneratorState(sequence)); - } - - private static string GetSequenceName(ISequence sequence, IRelationalConnection connection) - { - var dbConnection = connection.DbConnection; - - return dbConnection.Database.ToUpperInvariant() - + "::" - + dbConnection.DataSource.ToUpperInvariant() - + "::" - + (sequence.Schema is null ? "" : sequence.Schema + ".") - + sequence.Name; - } -} diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs deleted file mode 100644 index 808532b423..0000000000 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorSelector.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.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 NpgsqlValueGeneratorSelector : RelationalValueGeneratorSelector -{ - private readonly INpgsqlSequenceValueGeneratorFactory _sequenceFactory; - private readonly INpgsqlRelationalConnection _connection; - private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder; - private readonly IRelationalCommandDiagnosticsLogger _commandLogger; - - /// - /// 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 NpgsqlValueGeneratorSelector( - ValueGeneratorSelectorDependencies dependencies, - INpgsqlSequenceValueGeneratorFactory sequenceFactory, - INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder, - IRelationalCommandDiagnosticsLogger commandLogger) - : base(dependencies) - { - _sequenceFactory = sequenceFactory; - _connection = connection; - _rawSqlCommandBuilder = rawSqlCommandBuilder; - _commandLogger = commandLogger; - } - - /// - /// 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 new virtual INpgsqlValueGeneratorCache Cache - => (INpgsqlValueGeneratorCache)base.Cache; - - /// - /// 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 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 - /// 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 ValueGenerator? FindForType(IProperty property, ITypeBase typeBase, Type 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 deleted file mode 100644 index 428e93fab1..0000000000 --- a/src/EFCore.PG/ValueGeneration/NpgsqlSequentialGuidValueGenerator.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 -{ - /// - /// Gets a value to be assigned to a property. - /// - /// The change tracking entry of the entity for which the value is being generated. - /// The value to be assigned to a property. - public override Guid Next(EntityEntry entry) - => Guid.CreateVersion7(); - - /// - /// 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; -} diff --git a/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs b/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs deleted file mode 100644 index 8c4f595dca..0000000000 --- a/src/EFCore.PG/ValueGeneration/NpgsqlSequentialStringValueGenerator.cs +++ /dev/null @@ -1,22 +0,0 @@ -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/test/Directory.Build.props b/test/Directory.Build.props index b4dbbaa8a6..9e30577b5b 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -15,7 +15,6 @@ - diff --git a/test/EFCore.GaussDB.FunctionalTests/BadDataJsonDeserializationGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/BadDataJsonDeserializationGaussDBTest.cs new file mode 100644 index 0000000000..29f717fec3 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BadDataJsonDeserializationGaussDBTest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore; + +public class BadDataJsonDeserializationSqlServerTest : BadDataJsonDeserializationTestBase +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseGaussDB(b => b.UseNetTopologySuite())); +} diff --git a/test/EFCore.PG.FunctionalTests/BatchingTest.cs b/test/EFCore.GaussDB.FunctionalTests/BatchingTest.cs similarity index 95% rename from test/EFCore.PG.FunctionalTests/BatchingTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BatchingTest.cs index 9b0637e896..b123ff070a 100644 --- a/test/EFCore.PG.FunctionalTests/BatchingTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BatchingTest.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable InconsistentNaming @@ -154,9 +154,9 @@ public Task Inserts_are_batched_only_when_necessary(int minBatchSize) Assert.Contains( minBatchSize == 3 - ? RelationalResources.LogBatchReadyForExecution(new TestLogger()) + ? RelationalResources.LogBatchReadyForExecution(new TestLogger()) .GenerateMessage(3) - : RelationalResources.LogBatchSmallerThanMinBatchSize(new TestLogger()) + : RelationalResources.LogBatchSmallerThanMinBatchSize(new TestLogger()) .GenerateMessage(3, 4), Fixture.TestSqlLoggerFactory.Log.Select(l => l.Message)); @@ -248,7 +248,7 @@ public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; protected override Type ContextType { get; } = typeof(BloggingContext); @@ -261,7 +261,7 @@ protected override Task SeedAsync(PoolableDbContext context) public DbContext CreateContext(int minBatchSize) { var optionsBuilder = new DbContextOptionsBuilder(CreateOptions()); - new NpgsqlDbContextOptionsBuilder(optionsBuilder).MinBatchSize(minBatchSize); + new GaussDBDbContextOptionsBuilder(optionsBuilder).MinBatchSize(minBatchSize); return new BloggingContext(optionsBuilder.Options); } } diff --git a/test/EFCore.GaussDB.FunctionalTests/BuiltInDataTypesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/BuiltInDataTypesGaussDBTest.cs new file mode 100644 index 0000000000..aea0b903fc --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BuiltInDataTypesGaussDBTest.cs @@ -0,0 +1,1481 @@ +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations.Schema; +using System.Net.NetworkInformation; + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class BuiltInDataTypesGaussDBTest : BuiltInDataTypesTestBase +{ + // ReSharper disable once UnusedParameter.Local + public BuiltInDataTypesGaussDBTest(BuiltInDataTypesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [Fact] + public void Sql_translation_uses_type_mapper_when_constant() + { + using var context = CreateContext(); + var results = context.Set() + .Where(e => e.TimeSpanAsTime == new TimeSpan(0, 1, 2)) + .Select(e => e.Int) + .ToList(); + + Assert.Empty(results); + + AssertSql( + """ +SELECT m."Int" +FROM "MappedNullableDataTypes" AS m +WHERE m."TimeSpanAsTime" = TIME '00:01:02' +"""); + } + + [Fact] + public void Sql_translation_uses_type_mapper_when_parameter() + { + using var context = CreateContext(); + var timeSpan = new TimeSpan(2, 1, 0); + var results = context.Set() + .Where(e => e.TimeSpanAsTime == timeSpan) + .Select(e => e.Int) + .ToList(); + + Assert.Empty(results); + + AssertSql( + """ +@timeSpan='02:01:00' (Nullable = true) + +SELECT m."Int" +FROM "MappedNullableDataTypes" AS m +WHERE m."TimeSpanAsTime" = @timeSpan +"""); + } + + [Fact] + public virtual void Can_query_using_any_mapped_data_type() + { + using (var context = CreateContext()) + { + context.Set().Add( + new MappedNullableDataTypes + { + Int = 999, + LongAsBigint = 78L, + ShortAsSmallint = 79, + ByteAsSmallint = 80, + UintAsInt = uint.MaxValue, + UlongAsBigint = ulong.MaxValue, + UShortAsSmallint = ushort.MaxValue, + UintAsBigint = uint.MaxValue, + UShortAsInt = ushort.MaxValue, + BoolAsBoolean = true, + DecimalAsMoney = 81.1m, + Decimal = 101.7m, + DecimalAsNumeric = 103.9m, + BigIntegerAsNumeric = 105.4m, + FloatAsReal = 84.4f, + DoubleAsDoublePrecision = 85.5, + DateTimeAsTimestamp = new DateTime(2015, 1, 2, 10, 11, 12), + DateTimeAsTimestamptz = new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc), + DateTimeAsDate = new DateTime(2015, 1, 2, 0, 0, 0), + TimeSpanAsTime = new TimeSpan(11, 15, 12), + DateTimeOffsetAsTimetz = new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)), + TimeSpanAsInterval = new TimeSpan(11, 15, 12), + DateOnlyAsDate = new DateOnly(2015, 1, 2), + TimeOnlyAsTime = new TimeOnly(11, 15, 12), + StringAsText = "Gumball Rules!", + StringAsVarchar = "Gumball Rules OK", + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // CharAsChar1 = 'f', + CharAsText = 'g', + CharAsVarchar = 'h', + 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"), + GaussDBPointAsPoint = new GaussDBPoint(5.2, 3.3), + StringAsJsonb = """{"a": "b"}""", + StringAsJson = """{"a": "b"}""", + DictionaryAsHstore = new Dictionary { { "a", "b" } }, + ImmutableDictionaryAsHstore = ImmutableDictionary.Empty.Add("c", "d"), + GaussDBRangeAsRange = new GaussDBRange(4, true, 8, false), + IntArrayAsIntArray = [2, 3], + PhysicalAddressArrayAsMacaddrArray = + [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 = GaussDBTsQuery.Parse("a & b"), + SearchVector = GaussDBTsVector.Parse("a b"), +#pragma warning restore CS0618 + RankingNormalization = GaussDBTsRankingNormalization.DivideByLength, + Regconfig = 12724, + Mood = Mood.Sad + }); + + Assert.Equal(1, context.SaveChanges()); + } + + using (var context = CreateContext()) + { + var entity = context.Set().Single(e => e.Int == 999); + + long? param1 = 78L; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.LongAsBigint == param1)); + + short? param2 = 79; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.ShortAsSmallint == param2)); + + byte? param2a = 80; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.ByteAsSmallint == param2a)); + + uint? param3 = uint.MaxValue; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsInt == param3)); + + ulong? param4 = ulong.MaxValue; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UlongAsBigint == param4)); + + ushort? param5 = ushort.MaxValue; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UShortAsSmallint == param5)); + + uint? param6 = uint.MaxValue; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsBigint == param6)); + + ushort? param7 = ushort.MaxValue; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UShortAsInt == param7)); + + bool? param8 = true; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.BoolAsBoolean == param8)); + + // GaussDB doesn't support comparing money to decimal + //decimal? param9 = 81.1m; + //Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DecimalAsMoney == param9)); + + decimal? param10 = 101.7m; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Decimal == param10)); + + decimal? param11 = 103.9m; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DecimalAsNumeric == param11)); + + decimal? param12a = 105.4m; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.BigIntegerAsNumeric == param12a)); + + float? param12 = 84.4f; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.FloatAsReal == param12)); + + double? param13 = 85.5; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DoubleAsDoublePrecision == param13)); + + DateTime? param14 = new DateTime(2015, 1, 2, 10, 11, 12); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeAsTimestamp == param14)); + + DateTime? param15 = new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeAsTimestamptz == param15)); + + DateTime? param16 = new DateTime(2015, 1, 2, 0, 0, 0); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeAsDate == param16)); + + TimeSpan? param17 = new TimeSpan(11, 15, 12); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.TimeSpanAsTime == param17)); + + DateTimeOffset? param18 = new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeOffsetAsTimetz == param18)); + + TimeSpan? param19 = new TimeSpan(11, 15, 12); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.TimeSpanAsInterval == param19)); + + DateOnly? param20 = new DateOnly(2015, 1, 2); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateOnlyAsDate == param20)); + + TimeOnly? param21 = new TimeOnly(11, 15, 12); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.TimeOnlyAsTime == param21)); + + // ReSharper disable once ConvertToConstant.Local + var param22 = "Gumball Rules!"; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsText == param22)); + + // ReSharper disable once ConvertToConstant.Local + var param23 = "Gumball Rules OK"; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsVarchar == param23)); + + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // var param23a = 'f'; + // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.CharAsChar1 == param23a)); + + // ReSharper disable once ConvertToConstant.Local + var param23b = 'g'; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.CharAsText == param23b)); + + // ReSharper disable once ConvertToConstant.Local + var param23c = 'h'; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.CharAsVarchar == param23c)); + + var param24 = new byte[] { 86 }; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.BytesAsBytea == param24)); + + Guid? param25 = new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.GuidAsUuid == param25)); + + StringEnum16? param26 = StringEnum16.Value4; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.EnumAsText == param26)); + + StringEnumU16? param27 = StringEnumU16.Value4; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.EnumAsVarchar == param27)); + + var param28 = PhysicalAddress.Parse("08-00-2B-01-02-03"); + Assert.Same( + entity, context.Set().Single(e => e.Int == 999 && e.PhysicalAddressAsMacaddr.Equals(param28))); + + // GaussDB doesn't support equality comparison on point + // GaussDBPoint? param29 = new GaussDBPoint(5.2, 3.3); + // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Point == param29)); + + // ReSharper disable once ConvertToConstant.Local + 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) + // var param31 = @"{""a"": ""b""}"; + // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsJson == param31)); + + var param32 = new Dictionary { { "a", "b" } }; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DictionaryAsHstore == param32)); + + var param33 = ImmutableDictionary.Empty.Add("c", "d"); + Assert.Same( + entity, context.Set().Single(e => e.Int == 999 && e.ImmutableDictionaryAsHstore == param33)); + + var param34 = new GaussDBRange(4, true, 8, false); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.GaussDBRangeAsRange == param34)); + + var param35 = new[] { 2, 3 }; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.IntArrayAsIntArray == param35)); + + var param36 = new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }; + Assert.Same( + entity, + context.Set().Single(e => e.Int == 999 && e.PhysicalAddressArrayAsMacaddrArray == param36)); + + // ReSharper disable once ConvertToConstant.Local + var param37 = (uint)int.MaxValue + 1; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsXid == param37)); + +#pragma warning disable CS0618 // Full-text search client-parsing is obsolete + var param38 = GaussDBTsQuery.Parse("a & b"); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchQuery == param38)); + + var param39 = GaussDBTsVector.Parse("a b"); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchVector == param39)); +#pragma warning restore CS0618 + + // ReSharper disable once ConvertToConstant.Local + var param40 = GaussDBTsRankingNormalization.DivideByLength; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.RankingNormalization == param40)); + + // ReSharper disable once ConvertToConstant.Local + var param41 = 12724u; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Regconfig == param41)); + + // ReSharper disable once ConvertToConstant.Local + var param42 = Mood.Sad; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Mood == param42)); + } + } + + [Fact] + public virtual void Can_query_using_any_mapped_data_types_with_nulls() + { + using (var context = CreateContext()) + { + context.Set().Add( + new MappedNullableDataTypes { Int = 911, }); + + Assert.Equal(1, context.SaveChanges()); + } + + using (var context = CreateContext()) + { + var entity = context.Set().Single(e => e.Int == 911); + + long? param1 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.LongAsBigint == param1)); + + short? param2 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.ShortAsSmallint == param2)); + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && (long?)e.ShortAsSmallint == param2)); + + byte? param2a = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.ByteAsSmallint == param2a)); + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && (long?)e.ByteAsSmallint == param2a)); + + uint? param3 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsInt == param3)); + + ulong? param4 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UlongAsBigint == param4)); + + ushort? param5 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UShortAsSmallint == param5)); + + uint? param6 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsBigint == param6)); + + ushort? param7 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UShortAsInt == param7)); + + bool? param8 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.BoolAsBoolean == param8)); + + decimal? param9 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DecimalAsMoney == param9)); + + decimal? param10 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Decimal == param10)); + + decimal? param11 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DecimalAsNumeric == param11)); + + decimal? param111 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.BigIntegerAsNumeric == param111)); + + float? param12 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.FloatAsReal == param12)); + + double? param13 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DoubleAsDoublePrecision == param13)); + + DateTime? param14 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeAsTimestamp == param14)); + + DateTime? param15 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeAsTimestamptz == param15)); + + DateTime? param16 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeAsDate == param16)); + + TimeSpan? param17 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.TimeSpanAsTime == param17)); + + DateTimeOffset? param18 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeOffsetAsTimetz == param18)); + + TimeSpan? param19 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.TimeSpanAsInterval == param19)); + + DateOnly? param20 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateOnlyAsDate == param20)); + + TimeOnly? param21 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.TimeOnlyAsTime == param21)); + + string param22 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsText == param22)); + + string param23 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsVarchar == param23)); + + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // char? param23a = null; + // Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.CharAsChar1 == param23a)); + + char? param23b = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.CharAsText == param23b)); + + char? param23c = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.CharAsVarchar == param23c)); + + byte[] param24 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.BytesAsBytea == param24)); + + Guid? param25 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.GuidAsUuid == param25)); + + StringEnum16? param26 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.EnumAsText == param26)); + + StringEnumU16? param27 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.EnumAsVarchar == param27)); + + PhysicalAddress param28 = null; + // ReSharper disable once PossibleUnintendedReferenceComparison + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.PhysicalAddressAsMacaddr == param28)); + + // GaussDB does not support equality comparison on geometry types, see https://www.postgresql.org/docs/current/functions-geometry.html + //GaussDBPoint? param29 = null; + //Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.GaussDBPointAsPoint == param29)); + + string param30 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsJsonb == param30)); + + // GaussDB does not support equality comparison on json + //string param31 = null; + //Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsJson == param31)); + + Dictionary param32 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DictionaryAsHstore == param32)); + + ImmutableDictionary param33 = null; + Assert.Same( + entity, context.Set().Single(e => e.Int == 911 && e.ImmutableDictionaryAsHstore == param33)); + + GaussDBRange? param34 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.GaussDBRangeAsRange == param34)); + + int[] param35 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.IntArrayAsIntArray == param35)); + + PhysicalAddress[] param36 = null; + Assert.Same( + entity, + context.Set().Single(e => e.Int == 911 && e.PhysicalAddressArrayAsMacaddrArray == param36)); + + uint? param37 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsXid == param37)); + + GaussDBTsQuery param38 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchQuery == param38)); + + GaussDBTsVector param39 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchVector == param39)); + + GaussDBTsRankingNormalization? param40 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.RankingNormalization == param40)); + + uint? param41 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Regconfig == param41)); + + Mood? param42 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Mood == param42)); + } + } + + [Fact] + public virtual void Can_insert_and_read_back_all_mapped_data_types() + { + var entity = CreateMappedDataTypes(77); + using var context = CreateContext(); + context.Set().Add(entity); + + Assert.Equal(1, context.SaveChanges()); + + var parameters = DumpParameters(); + Assert.Equal( + """ +@p0='77' +@p1='True' +@p2='80' (DbType = Int16) +@p3='0x56' (Nullable = false) +@p4='g' (Nullable = false) +@p5='h' (Nullable = false) +@p6='2015-01-02T00:00:00.0000000' (DbType = Date) +@p7='2015-01-02T10:11:12.0000000' +@p8='2016-01-02T11:11:12.0000000Z' (DbType = DateTime) +@p9='0001-01-01T12:00:00.0000000+02:00' (DbType = Object) +@p10='101.7' +@p11='81.1' (DbType = Currency) +@p12='103.9' +@p13='System.Collections.Generic.Dictionary`2[System.String,System.String]' (Nullable = false) (DbType = Object) +@p14='85.5' +@p15='Value4' (Nullable = false) +@p16='Value4' (Nullable = false) +@p17='84.4' +@p18='a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +@p19={ '2' +'3' } (Nullable = false) (DbType = Object) +@p20='78' +@p21='Sad' (DbType = Object) +@p22='(5.2,3.3)' (DbType = Object) +@p23='[4,8)' (DbType = Object) +@p24={ '08002B010203' +'08002B010204' } (Nullable = false) (DbType = Object) +@p25='08002B010203' (Nullable = false) (DbType = Object) +@p26='2' +@p27='12724' (DbType = Object) +@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) +@p33='Gumball Rules!' (Nullable = false) +@p34='Gumball Rules OK' (Nullable = false) +@p35='11:15:12' (DbType = Object) +@p36='11:15:12' +@p37='65535' +@p38='-1' +@p39='4294967295' +@p40='-1' +@p41='2147483648' (DbType = Object) +@p42='-1' +""", + parameters, + ignoreLineEndingDifferences: true); + } + + private string DumpParameters() + => Fixture.TestSqlLoggerFactory.Parameters.Single().Replace(", ", Environment.NewLine); + + // ReSharper disable once UnusedMember.Local + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private static void AssertMappedDataTypes(MappedDataTypes entity, int id) + { + // ReSharper disable once UnusedVariable + var expected = CreateMappedDataTypes(id); + Assert.Equal(id, entity.Int); + Assert.Equal(78, entity.LongAsBigint); + Assert.Equal(79, entity.ShortAsSmallint); + Assert.Equal(uint.MaxValue, entity.UintAsInt); + Assert.Equal(ulong.MaxValue, entity.UlongAsBigint); + Assert.Equal(ushort.MaxValue, entity.UShortAsSmallint); + Assert.Equal(uint.MaxValue, entity.UintAsBigint); + Assert.Equal(ushort.MaxValue, entity.UShortAsInt); + + Assert.True(entity.BoolAsBoolean); + + Assert.Equal(81.1m, entity.DecimalAsMoney); + Assert.Equal(101.7m, entity.Decimal); + Assert.Equal(103.9m, entity.DecimalAsNumeric); + Assert.Equal(84.4f, entity.FloatAsReal); + Assert.Equal(85.5, entity.DoubleAsDoublePrecision); + + Assert.Equal(new DateTime(2015, 1, 2, 10, 11, 12), entity.DateTimeAsTimestamp); + Assert.Equal(new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc), entity.DateTimeAsTimestamptz); + Assert.Equal(new DateTime(2015, 1, 2, 0, 0, 0), entity.DateTimeAsDate); + Assert.Equal(new TimeSpan(11, 15, 12), entity.TimeSpanAsTime); + Assert.Equal(new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)), entity.DateTimeOffsetAsTimetz); + Assert.Equal(new TimeSpan(11, 15, 12), entity.TimeSpanAsInterval); + + Assert.Equal("Gumball Rules!", entity.StringAsText); + Assert.Equal("Gumball Rules OK", entity.StringAsVarchar); + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // Assert.Equal('f', entity.CharAsChar1); + Assert.Equal('g', entity.CharAsText); + Assert.Equal('h', entity.CharAsVarchar); + Assert.Equal([86], entity.BytesAsBytea); + + Assert.Equal(new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), entity.GuidAsUuid); + + Assert.Equal(StringEnum16.Value4, entity.EnumAsText); + Assert.Equal(StringEnumU16.Value4, entity.EnumAsVarchar); + + Assert.Equal(PhysicalAddress.Parse("08-00-2B-01-02-03"), entity.PhysicalAddressAsMacaddr); + Assert.Equal(new GaussDBPoint(5.2, 3.3), entity.GaussDBPointAsPoint); + Assert.Equal("""{"a": "b"}""", entity.StringAsJsonb); + Assert.Equal("""{"a": "b"}""", entity.StringAsJson); + Assert.Equal(new Dictionary { { "a", "b" } }, entity.DictionaryAsHstore); + Assert.Equal(new GaussDBRange(4, true, 8, false), entity.GaussDBRangeAsRange); + + Assert.Equal(new[] { 2, 3 }, entity.IntArrayAsIntArray); + Assert.Equal( + [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); + +#pragma warning disable CS0618 // Full-text search client-parsing is obsolete + Assert.Equal(GaussDBTsQuery.Parse("a & b").ToString(), entity.SearchQuery.ToString()); + Assert.Equal(GaussDBTsVector.Parse("a b").ToString(), entity.SearchVector.ToString()); +#pragma warning restore CS0618 + Assert.Equal(GaussDBTsRankingNormalization.DivideByLength, entity.RankingNormalization); + } + + private static MappedDataTypes CreateMappedDataTypes(int id) + => new() + { + Int = id, + LongAsBigint = 78L, + ShortAsSmallint = 79, + ByteAsSmallint = 80, + UintAsInt = uint.MaxValue, + UlongAsBigint = ulong.MaxValue, + UShortAsSmallint = ushort.MaxValue, + UintAsBigint = uint.MaxValue, + UShortAsInt = ushort.MaxValue, + BoolAsBoolean = true, + DecimalAsMoney = 81.1m, + Decimal = 101.7m, + DecimalAsNumeric = 103.9m, + FloatAsReal = 84.4f, + DoubleAsDoublePrecision = 85.5, + DateTimeAsTimestamp = new DateTime(2015, 1, 2, 10, 11, 12), + DateTimeAsTimestamptz = new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc), + DateTimeAsDate = new DateTime(2015, 1, 2, 0, 0, 0), + TimeSpanAsTime = new TimeSpan(11, 15, 12), + DateTimeOffsetAsTimetz = new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)), + TimeSpanAsInterval = new TimeSpan(11, 15, 12), + StringAsText = "Gumball Rules!", + StringAsVarchar = "Gumball Rules OK", + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // CharAsChar1 = 'f', + CharAsText = 'g', + CharAsVarchar = 'h', + 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"), + GaussDBPointAsPoint = new GaussDBPoint(5.2, 3.3), + StringAsJsonb = """{"a": "b"}""", + StringAsJson = """{"a": "b"}""", + DictionaryAsHstore = new Dictionary { { "a", "b" } }, + GaussDBRangeAsRange = new GaussDBRange(4, true, 8, false), + IntArrayAsIntArray = [2, 3], + PhysicalAddressArrayAsMacaddrArray = + [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 = GaussDBTsQuery.Parse("a & b"), + SearchVector = GaussDBTsVector.Parse("a b"), +#pragma warning restore CS0618 + RankingNormalization = GaussDBTsRankingNormalization.DivideByLength, + Regconfig = 12724, + Mood = Mood.Sad + }; + + [Fact] + public virtual void Can_insert_and_read_back_all_mapped_data_types_set_to_null() + { + using (var context = CreateContext()) + { + context.Set().Add(new MappedNullableDataTypes { Int = 78 }); + + Assert.Equal(1, context.SaveChanges()); + } + + using (var context = CreateContext()) + { + AssertNullMappedNullableDataTypes(context.Set().Single(e => e.Int == 78), 78); + } + } + + // ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local + private static void AssertNullMappedNullableDataTypes(MappedNullableDataTypes entity, int id) + // ReSharper restore ParameterOnlyUsedForPreconditionCheck.Local + { + Assert.Equal(id, entity.Int); + Assert.Null(entity.LongAsBigint); + Assert.Null(entity.ShortAsSmallint); + Assert.Null(entity.ByteAsSmallint); + Assert.Null(entity.UintAsInt); + Assert.Null(entity.UlongAsBigint); + Assert.Null(entity.UShortAsSmallint); + Assert.Null(entity.UintAsBigint); + Assert.Null(entity.UShortAsInt); + + Assert.Null(entity.BoolAsBoolean); + + Assert.Null(entity.DecimalAsMoney); + Assert.Null(entity.Decimal); + Assert.Null(entity.DecimalAsNumeric); + Assert.Null(entity.FloatAsReal); + Assert.Null(entity.DoubleAsDoublePrecision); + + Assert.Null(entity.DateTimeAsTimestamp); + Assert.Null(entity.DateTimeAsTimestamptz); + Assert.Null(entity.DateTimeAsDate); + Assert.Null(entity.TimeSpanAsTime); + Assert.Null(entity.DateTimeOffsetAsTimetz); + Assert.Null(entity.TimeSpanAsInterval); + + Assert.Null(entity.DateOnlyAsDate); + Assert.Null(entity.TimeOnlyAsTime); + + Assert.Null(entity.StringAsText); + Assert.Null(entity.StringAsVarchar); + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // Assert.Null(entity.CharAsChar1); + Assert.Null(entity.CharAsText); + Assert.Null(entity.CharAsVarchar); + Assert.Null(entity.BytesAsBytea); + + Assert.Null(entity.GuidAsUuid); + + Assert.Null(entity.EnumAsText); + Assert.Null(entity.EnumAsVarchar); + + Assert.Null(entity.PhysicalAddressAsMacaddr); + Assert.Null(entity.GaussDBPointAsPoint); + Assert.Null(entity.StringAsJsonb); + Assert.Null(entity.StringAsJson); + Assert.Null(entity.DictionaryAsHstore); + Assert.Null(entity.ImmutableDictionaryAsHstore); + Assert.Null(entity.GaussDBRangeAsRange); + + Assert.Null(entity.IntArrayAsIntArray); + Assert.Null(entity.PhysicalAddressArrayAsMacaddrArray); + + Assert.Null(entity.UintAsXid); + + Assert.Null(entity.SearchQuery); + Assert.Null(entity.SearchVector); + Assert.Null(entity.RankingNormalization); + + Assert.Null(entity.Mood); + } + + 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, await context.SaveChangesAsync()); + } + + using (var context = CreateContext()) + { + var entity = (await context.Set().Where(e => e.Id == 711).ToListAsync()).Single(); + + short? param1 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt16 == param1).ToListAsync()) + .Single()); + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && (long?)e.TestNullableInt16 == param1) + .ToListAsync()) + .Single()); + + int? param2 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt32 == param2).ToListAsync()) + .Single()); + + long? param3 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt64 == param3).ToListAsync()) + .Single()); + + double? param4 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableDouble == param4).ToListAsync()) + .Single()); + + decimal? param5 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableDecimal == param5).ToListAsync()) + .Single()); + + DateTime? param6 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableDateTime == param6).ToListAsync()) + .Single()); + + // We don't support DateTimeOffset + + TimeSpan? param8 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableTimeSpan == param8).ToListAsync()) + .Single()); + + float? param9 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableSingle == param9).ToListAsync()) + .Single()); + + bool? param10 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableBoolean == param10).ToListAsync()) + .Single()); + + // We don't support byte + + Enum64? param12 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum64 == param12).ToListAsync()).Single()); + + Enum32? param13 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum32 == param13).ToListAsync()).Single()); + + Enum16? param14 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.Enum16 == param14).ToListAsync()).Single()); + + Enum8? param15 = null; + Assert.Same( + 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) + { + ushort? param16 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt16 == param16) + .ToListAsync()) + .Single()); + } + + if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt32)) is not null) + { + uint? param17 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt32 == param17) + .ToListAsync()) + .Single()); + } + + if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt64)) is not null) + { + ulong? param18 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt64 == param18) + .ToListAsync()) + .Single()); + } + + if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableCharacter)) is not null) + { + char? param19 = null; + Assert.Same( + entity, + (await context.Set().Where(e => e.Id == 711 && e.TestNullableCharacter == param19) + .ToListAsync()) + .Single()); + } + + if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableSignedByte)) is not null) + { + sbyte? param20 = null; + Assert.Same( + entity, + (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, + (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, + (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, + (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, + (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 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 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 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 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 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 Task Can_query_using_any_data_type_shadow() + => Task.CompletedTask; + + [ConditionalFact] + public void Sum_Conversions() + { + using var context = CreateContext(); + + // GaussDB SUM() returns numeric for bigint input, bigint for int/smallint ints. + // Make sure the proper conversion is done + _ = context.Set().Sum(m => m.LongAsBigint); + _ = context.Set().Sum(m => m.Int); + _ = context.Set().Sum(m => m.ShortAsSmallint); + + AssertSql( + """ +SELECT COALESCE(sum(m."LongAsBigint"), 0.0)::bigint +FROM "MappedDataTypes" AS m +""", + // + """ +SELECT COALESCE(sum(m."Int"), 0)::int +FROM "MappedDataTypes" AS m +""", + // + """ +SELECT COALESCE(sum(m."ShortAsSmallint"::int), 0)::int +FROM "MappedDataTypes" AS m +"""); + } + + [ConditionalFact] + public void Money_compare_constant() + { + using var context = CreateContext(); + + _ = context.Set().Where(m => m.DecimalAsMoney > 3).ToList(); + } + + [ConditionalFact] + public void Money_compare_parameter() + { + using var context = CreateContext(); + + var money = 3m; + _ = context.Set().Where(m => m.DecimalAsMoney > money).ToList(); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class BuiltInDataTypesGaussDBFixture : BuiltInDataTypesFixtureBase + { + public override bool StrictEquality + => false; + + public override bool SupportsAnsi + => false; + + public override bool SupportsUnicodeToAnsiConversion + => false; + + public override bool SupportsLargeStringComparisons + => true; + + public override bool SupportsDecimalComparisons + => true; + + public override bool PreservesDateTimeKind + => false; + + // We instruct the test store to pass a connection string to UseGaussDB() instead of a DbConnection - that's required to allow + // EF's MapEnum() to function properly and instantiate an GaussDBDataSource internally. + protected override ITestStoreFactory TestStoreFactory + => new GaussDBTestStoreFactory(useConnectionString: true); + + protected override bool ShouldLogCategory(string logCategory) + => logCategory == DbLoggerCategory.Query.Name; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).UseGaussDB(o => o.MapEnum("mood")); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + MakeRequired(modelBuilder); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(b => b.TestDateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.TestNullableDateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(nameof(BuiltInNullableDataTypes.TestNullableDateTime)) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + + // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data + 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); + + 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); + + modelBuilder.Entity().Metadata.GetSeedData() + .Single()[nameof(ObjectBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(new DateTime(), TimeSpan.Zero); + + modelBuilder.Entity().Metadata.GetSeedData() + .Single()[nameof(NullableBackedDataTypes.DateTimeOffset)] + = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.Zero); + + modelBuilder.Entity().Metadata.GetSeedData() + .Single()[nameof(NonNullableBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(new DateTime(), TimeSpan.Zero); + + modelBuilder.Entity( + b => + { + b.Ignore(dt => dt.TestUnsignedInt16); + b.Ignore(dt => dt.TestUnsignedInt32); + b.Ignore(dt => dt.TestUnsignedInt64); + b.Ignore(dt => dt.TestCharacter); + b.Ignore(dt => dt.TestSignedByte); + b.Ignore(dt => dt.TestDateTimeOffset); + b.Ignore(dt => dt.TestByte); + //b.Ignore(dt => dt.EnumU16); + //b.Ignore(dt => dt.EnumU32); + //b.Ignore(dt => dt.EnumU64); + //b.Ignore(dt => dt.EnumS8); + }); + + modelBuilder.Entity( + b => + { + b.Ignore(dt => dt.TestNullableUnsignedInt16); + b.Ignore(dt => dt.TestNullableUnsignedInt32); + b.Ignore(dt => dt.TestNullableUnsignedInt64); + b.Ignore(dt => dt.TestNullableCharacter); + b.Ignore(dt => dt.TestNullableSignedByte); + b.Ignore(dt => dt.TestNullableDateTimeOffset); + b.Ignore(dt => dt.TestNullableByte); + //b.Ignore(dt => dt.EnumU16); + //b.Ignore(dt => dt.EnumU32); + //b.Ignore(dt => dt.EnumU64); + //b.Ignore(dt => dt.EnumS8); + }); + + modelBuilder.Entity( + b => + { + b.HasKey(e => e.Int); + b.Property(e => e.Int).ValueGeneratedNever(); + }); + + modelBuilder.Entity( + b => + { + b.HasKey(e => e.Int); + b.Property(e => e.Int).ValueGeneratedNever(); + }); + + modelBuilder.Entity() + .Property(e => e.Id) + .ValueGeneratedNever(); + + modelBuilder.Entity() + .Property(e => e.Id) + .ValueGeneratedNever(); + + modelBuilder.Entity() + .Property(e => e.Id) + .ValueGeneratedNever(); + + // Full text + modelBuilder.Entity().Property(x => x.SearchQuery).HasColumnType("tsquery"); + modelBuilder.Entity().Property(x => x.SearchVector).HasColumnType("tsvector"); + modelBuilder.Entity().Property(x => x.RankingNormalization).HasColumnType("integer"); + modelBuilder.Entity().Property(x => x.Regconfig).HasColumnType("regconfig"); + modelBuilder.Entity().Property(x => x.SearchQuery).HasColumnType("tsquery"); + modelBuilder.Entity().Property(x => x.SearchVector).HasColumnType("tsvector"); + modelBuilder.Entity().Property(x => x.RankingNormalization).HasColumnType("integer"); + modelBuilder.Entity().Property(x => x.Regconfig).HasColumnType("regconfig"); + } + + public override bool SupportsBinaryKeys + => true; + + public override DateTime DefaultDateTime + => new(); + } + + protected enum StringEnum16 : short + { + // ReSharper disable once UnusedMember.Global + Value1 = 1, + + // ReSharper disable once UnusedMember.Global + Value2 = 2, + Value4 = 4 + } + + protected enum StringEnumU16 : ushort + { + // ReSharper disable once UnusedMember.Global + Value1 = 1, + + // ReSharper disable once UnusedMember.Global + Value2 = 2, + Value4 = 4 + } + + // ReSharper disable once MemberCanBePrivate.Global + protected class MappedDataTypes + { + [Column(TypeName = "int")] + public int Int { get; set; } + + [Column(TypeName = "bigint")] + public long LongAsBigint { get; set; } + + [Column(TypeName = "smallint")] + public short ShortAsSmallint { get; set; } + + [Column(TypeName = "smallint")] + public byte ByteAsSmallint { get; set; } + + [Column(TypeName = "int")] + public uint UintAsInt { get; set; } + + [Column(TypeName = "bigint")] + public uint UintAsBigint { get; set; } + + [Column(TypeName = "bigint")] + public ulong UlongAsBigint { get; set; } + + [Column(TypeName = "smallint")] + public ushort UShortAsSmallint { get; set; } + + [Column(TypeName = "int")] + public ushort UShortAsInt { get; set; } + + //[Column(TypeName = "tinyint")] + //public sbyte SByteAsTinyint { get; set; } + + [Column(TypeName = "boolean")] + public bool BoolAsBoolean { get; set; } + + [Column(TypeName = "numeric")] + public decimal Decimal { get; set; } // decimal is just an alias for numeric + + [Column(TypeName = "numeric")] + public decimal DecimalAsNumeric { get; set; } + + [Column(TypeName = "money")] + public decimal DecimalAsMoney { get; set; } + + [Column(TypeName = "double precision")] + public double DoubleAsDoublePrecision { get; set; } + + [Column(TypeName = "real")] + public float FloatAsReal { get; set; } + + [Column(TypeName = "timestamp")] + public DateTime DateTimeAsTimestamp { get; set; } + + [Column(TypeName = "timestamptz")] + public DateTime DateTimeAsTimestamptz { get; set; } + + [Column(TypeName = "date")] + public DateTime DateTimeAsDate { get; set; } + + [Column(TypeName = "time")] + public TimeSpan TimeSpanAsTime { get; set; } + + [Column(TypeName = "timetz")] + public DateTimeOffset DateTimeOffsetAsTimetz { get; set; } + + [Column(TypeName = "interval")] + public TimeSpan TimeSpanAsInterval { get; set; } + + [Column(TypeName = "text")] + public string StringAsText { get; set; } + + [Column(TypeName = "varchar")] + public string StringAsVarchar { get; set; } + + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // [Column(TypeName = "char(1)")] + // public char? CharAsChar1 { get; set; } + + [Column(TypeName = "text")] + public char? CharAsText { get; set; } + + [Column(TypeName = "varchar")] + public char? CharAsVarchar { get; set; } + + [Column(TypeName = "bytea")] + public byte[] BytesAsBytea { get; set; } + + [Column(TypeName = "uuid")] + public Guid GuidAsUuid { get; set; } + + [Column(TypeName = "text")] + public StringEnum16 EnumAsText { get; set; } + + [Column(TypeName = "varchar")] + public StringEnumU16 EnumAsVarchar { get; set; } + + // GaussDB-specific types from here + + [Column(TypeName = "macaddr")] + public PhysicalAddress PhysicalAddressAsMacaddr { get; set; } + + [Column(TypeName = "point")] + public GaussDBPoint GaussDBPointAsPoint { get; set; } + + [Column(TypeName = "jsonb")] + public string StringAsJsonb { get; set; } + + [Column(TypeName = "json")] + public string StringAsJson { get; set; } + + [Column(TypeName = "hstore")] + public Dictionary DictionaryAsHstore { get; set; } + + [Column(TypeName = "int4range")] + public GaussDBRange GaussDBRangeAsRange { get; set; } + + [Column(TypeName = "int[]")] + public int[] IntArrayAsIntArray { get; set; } + + [Column(TypeName = "macaddr[]")] + public PhysicalAddress[] PhysicalAddressArrayAsMacaddrArray { get; set; } + + [Column(TypeName = "xid")] + public uint UintAsXid { get; set; } + + public GaussDBTsQuery SearchQuery { get; set; } + public GaussDBTsVector SearchVector { get; set; } + public GaussDBTsRankingNormalization RankingNormalization { get; set; } + public uint Regconfig { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + [Column(TypeName = "mood")] + public Mood Mood { get; set; } + } + + // ReSharper disable once MemberCanBePrivate.Global + public class MappedSizedDataTypes + { + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public int Id { get; set; } + /* + public string Char { get; set; } + public string Character { get; set; } + public string Varchar { get; set; } + public string Char_varying { get; set; } + public string Character_varying { get; set; } + public string Nchar { get; set; } + public string National_character { get; set; } + public string Nvarchar { get; set; } + public string National_char_varying { get; set; } + public string National_character_varying { get; set; } + public byte[] Binary { get; set; } + public byte[] Varbinary { get; set; } + public byte[] Binary_varying { get; set; } + */ + } + + // ReSharper disable once MemberCanBePrivate.Global + public class MappedScaledDataTypes + { + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public int Id { get; set; } + /* + public float Float { get; set; } + public float Double_precision { get; set; } + public DateTimeOffset Datetimeoffset { get; set; } + public DateTime Datetime2 { get; set; } + public decimal Decimal { get; set; } + public decimal Dec { get; set; } + public decimal Numeric { get; set; } + */ + } + + // ReSharper disable once MemberCanBePrivate.Global + public class MappedPrecisionAndScaledDataTypes + { + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public int Id { get; set; } + /* + public decimal Decimal { get; set; } + public decimal Dec { get; set; } + public decimal Numeric { get; set; } + */ + } + + // ReSharper disable once MemberCanBePrivate.Global + protected class MappedNullableDataTypes + { + [Column(TypeName = "int")] + public int? Int { get; set; } + + [Column(TypeName = "bigint")] + public long? LongAsBigint { get; set; } + + [Column(TypeName = "smallint")] + public short? ShortAsSmallint { get; set; } + + [Column(TypeName = "smallint")] + public byte? ByteAsSmallint { get; set; } + + [Column(TypeName = "int")] + public uint? UintAsInt { get; set; } + + [Column(TypeName = "bigint")] + public uint? UintAsBigint { get; set; } + + [Column(TypeName = "bigint")] + public ulong? UlongAsBigint { get; set; } + + [Column(TypeName = "smallint")] + public ushort? UShortAsSmallint { get; set; } + + [Column(TypeName = "int")] + public ushort? UShortAsInt { get; set; } + + //[Column(TypeName = "tinyint")] + //public sbyte? SByteAsTinyint { get; set; } + + [Column(TypeName = "boolean")] + public bool? BoolAsBoolean { get; set; } + + [Column(TypeName = "numeric")] + public decimal? Decimal { get; set; } // decimal is just an alias for numeric + + [Column(TypeName = "numeric")] + public decimal? DecimalAsNumeric { get; set; } + + [Column(TypeName = "money")] + public decimal? DecimalAsMoney { get; set; } + + [Column(TypeName = "numeric")] + public decimal? BigIntegerAsNumeric { get; set; } + + [Column(TypeName = "double precision")] + public double? DoubleAsDoublePrecision { get; set; } + + [Column(TypeName = "real")] + public float? FloatAsReal { get; set; } + + [Column(TypeName = "timestamp")] + public DateTime? DateTimeAsTimestamp { get; set; } + + [Column(TypeName = "timestamptz")] + public DateTime? DateTimeAsTimestamptz { get; set; } + + [Column(TypeName = "date")] + public DateTime? DateTimeAsDate { get; set; } + + [Column(TypeName = "time")] + public TimeSpan? TimeSpanAsTime { get; set; } + + [Column(TypeName = "date")] + public DateOnly? DateOnlyAsDate { get; set; } + + [Column(TypeName = "time")] + public TimeOnly? TimeOnlyAsTime { get; set; } + + [Column(TypeName = "timetz")] + public DateTimeOffset? DateTimeOffsetAsTimetz { get; set; } + + [Column(TypeName = "interval")] + public TimeSpan? TimeSpanAsInterval { get; set; } + + [Column(TypeName = "text")] + public string StringAsText { get; set; } + + [Column(TypeName = "varchar")] + public string StringAsVarchar { get; set; } + + // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed + // [Column(TypeName = "char(1)")] + // public char? CharAsChar1 { get; set; } + + [Column(TypeName = "text")] + public char? CharAsText { get; set; } + + [Column(TypeName = "varchar")] + public char? CharAsVarchar { get; set; } + + [Column(TypeName = "bytea")] + public byte[] BytesAsBytea { get; set; } + + [Column(TypeName = "uuid")] + public Guid? GuidAsUuid { get; set; } + + [Column(TypeName = "text")] + public StringEnum16? EnumAsText { get; set; } + + [Column(TypeName = "varchar")] + public StringEnumU16? EnumAsVarchar { get; set; } + + // GaussDB-specific types from here + + [Column(TypeName = "macaddr")] + public PhysicalAddress PhysicalAddressAsMacaddr { get; set; } + + [Column(TypeName = "point")] + public GaussDBPoint? GaussDBPointAsPoint { get; set; } + + [Column(TypeName = "jsonb")] + public string StringAsJsonb { get; set; } + + [Column(TypeName = "json")] + public string StringAsJson { get; set; } + + [Column(TypeName = "hstore")] + public Dictionary DictionaryAsHstore { get; set; } + + [Column(TypeName = "hstore")] + public ImmutableDictionary ImmutableDictionaryAsHstore { get; set; } + + [Column(TypeName = "int4range")] + public GaussDBRange? GaussDBRangeAsRange { get; set; } + + [Column(TypeName = "int[]")] + public int[] IntArrayAsIntArray { get; set; } + + [Column(TypeName = "macaddr[]")] + public PhysicalAddress[] PhysicalAddressArrayAsMacaddrArray { get; set; } + + [Column(TypeName = "xid")] + public uint? UintAsXid { get; set; } + + public GaussDBTsQuery SearchQuery { get; set; } + public GaussDBTsVector SearchVector { get; set; } + public GaussDBTsRankingNormalization? RankingNormalization { get; set; } + public uint? Regconfig { get; set; } + + [Column(TypeName = "mood")] + public Mood? Mood { get; set; } + } +} + +// ReSharper disable once UnusedMember.Global +public enum Mood { Happy, Sad } diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs new file mode 100644 index 0000000000..21c9f95588 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,253 @@ +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class ComplexTypeBulkUpdatesGaussDBTest( + ComplexTypeBulkUpdatesGaussDBTest.ComplexTypeBulkUpdatesGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : ComplexTypeBulkUpdatesRelationalTestBase(fixture, testOutputHelper) +{ + public override async Task Delete_entity_type_with_complex_type(bool async) + { + await base.Delete_entity_type_with_complex_type(async); + + AssertSql( + """ +DELETE FROM "Customer" AS c +WHERE c."Name" = 'Monty Elias' +"""); + } + + public override async Task Delete_complex_type(bool async) + { + await base.Delete_complex_type(async); + + AssertSql(); + } + + public override async Task Update_property_inside_complex_type(bool async) + { + await base.Update_property_inside_complex_type(async); + + AssertExecuteUpdateSql( + """ +@p='12345' + +UPDATE "Customer" AS c +SET "ShippingAddress_ZipCode" = @p +WHERE c."ShippingAddress_ZipCode" = 7728 +"""); + } + + public override async Task Update_property_inside_nested_complex_type(bool async) + { + await base.Update_property_inside_nested_complex_type(async); + + AssertExecuteUpdateSql( + """ +@p='United States Modified' + +UPDATE "Customer" AS c +SET "ShippingAddress_Country_FullName" = @p +WHERE c."ShippingAddress_Country_Code" = 'US' +"""); + } + + public override async Task Update_multiple_properties_inside_multiple_complex_types_and_on_entity_type(bool async) + { + await base.Update_multiple_properties_inside_multiple_complex_types_and_on_entity_type(async); + + AssertExecuteUpdateSql( + """ +@p='54321' + +UPDATE "Customer" AS c +SET "Name" = c."Name" || 'Modified', + "ShippingAddress_ZipCode" = c."BillingAddress_ZipCode", + "BillingAddress_ZipCode" = @p +WHERE c."ShippingAddress_ZipCode" = 7728 +"""); + } + + public override async Task Update_projected_complex_type(bool async) + { + await base.Update_projected_complex_type(async); + + AssertExecuteUpdateSql( + """ +@p='12345' + +UPDATE "Customer" AS c +SET "ShippingAddress_ZipCode" = @p +"""); + } + + public override async Task Update_multiple_projected_complex_types_via_anonymous_type(bool async) + { + await base.Update_multiple_projected_complex_types_via_anonymous_type(async); + + AssertExecuteUpdateSql( + """ +@p='54321' + +UPDATE "Customer" AS c +SET "ShippingAddress_ZipCode" = c."BillingAddress_ZipCode", + "BillingAddress_ZipCode" = @p +"""); + } + + public override async Task Update_projected_complex_type_via_OrderBy_Skip(bool 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_p_AddressLine1='New AddressLine1' +@complex_type_p_AddressLine2='New AddressLine2' +@complex_type_p_Tags={ 'new_tag1', 'new_tag2' } (DbType = Object) +@complex_type_p_ZipCode='99999' (Nullable = true) +@complex_type_p_Code='FR' +@complex_type_p_FullName='France' + +UPDATE "Customer" AS c +SET "ShippingAddress_AddressLine1" = @complex_type_p_AddressLine1, + "ShippingAddress_AddressLine2" = @complex_type_p_AddressLine2, + "ShippingAddress_Tags" = @complex_type_p_Tags, + "ShippingAddress_ZipCode" = @complex_type_p_ZipCode, + "ShippingAddress_Country_Code" = @complex_type_p_Code, + "ShippingAddress_Country_FullName" = @complex_type_p_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_p_Code='FR' +@complex_type_p_FullName='France' + +UPDATE "Customer" AS c +SET "ShippingAddress_Country_Code" = @complex_type_p_Code, + "ShippingAddress_Country_FullName" = @complex_type_p_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( + """ +@complex_type_p_AddressLine1='New AddressLine1' +@complex_type_p_AddressLine2='New AddressLine2' +@complex_type_p_Tags={ 'new_tag1', 'new_tag2' } (DbType = Object) +@complex_type_p_ZipCode='99999' (Nullable = true) +@complex_type_p_Code='FR' +@complex_type_p_FullName='France' + +UPDATE "Customer" AS c +SET "ShippingAddress_AddressLine1" = @complex_type_p_AddressLine1, + "ShippingAddress_AddressLine2" = @complex_type_p_AddressLine2, + "ShippingAddress_Tags" = @complex_type_p_Tags, + "ShippingAddress_ZipCode" = @complex_type_p_ZipCode, + "ShippingAddress_Country_Code" = @complex_type_p_Code, + "ShippingAddress_Country_FullName" = @complex_type_p_FullName +"""); + } + + 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='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 +) 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( + """ +@p={ 'new_tag1', 'new_tag2' } (DbType = Object) + +UPDATE "Customer" AS c +SET "ShippingAddress_Tags" = @p +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + public class ComplexTypeBulkUpdatesGaussDBFixture : ComplexTypeBulkUpdatesRelationalFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs similarity index 95% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs index 96616ab1d8..a7a311c569 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs @@ -1,9 +1,9 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class NonSharedModelBulkUpdatesNpgsqlTest(NonSharedFixture fixture) : NonSharedModelBulkUpdatesRelationalTestBase(fixture) +public class NonSharedModelBulkUpdatesGaussDBTest(NonSharedFixture fixture) : NonSharedModelBulkUpdatesRelationalTestBase(fixture) { protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; [ConditionalFact] public virtual void Check_all_tests_overridden() @@ -138,7 +138,7 @@ public override async Task Update_owned_and_non_owned_properties_with_table_shar public override async Task Update_main_table_in_entity_with_entity_splitting(bool async) { - // Overridden/duplicated because we update DateTime, which Npgsql requires to be a UTC timestamp + // Overridden/duplicated because we update DateTime, which GaussDB requires to be a UTC timestamp var contextFactory = await InitializeAsync( onModelCreating: mb => mb.Entity() .ToTable("Blogs") @@ -212,7 +212,7 @@ SELECT COALESCE(sum(o0."Amount"), 0)::int """); } - [ConditionalTheory] // #3622 + [ConditionalTheory] // #3001 [MemberData(nameof(IsAsyncData))] public virtual async Task Update_with_primitive_collection_in_value_selector(bool async) { @@ -223,12 +223,12 @@ public virtual async Task Update_with_primitive_collection_in_value_selector(boo await ctx.SaveChangesAsync(); }); - await Assert.ThrowsAsync(() => AssertUpdate( + await AssertUpdate( async, contextFactory.CreateContext, ss => ss.EntitiesWithPrimitiveCollection, s => s.SetProperty(x => x.Tags, x => x.Tags.Append("another_tag")), - rowsAffectedCount: 1)); + rowsAffectedCount: 1); } protected class Context3001(DbContextOptions options) : DbContext(options) diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 0000000000..e261a9bb2c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class NorthwindBulkUpdatesGaussDBFixture : NorthwindBulkUpdatesRelationalFixture + where TModelCustomizer : ITestModelCustomizer, new() +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBNorthwindTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity() + .Property(p => p.UnitPrice) + .HasColumnType("money"); + } + + protected override Type ContextType + => typeof(NorthwindGaussDBContext); +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs index 30494430c2..000e374ee0 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs @@ -4,10 +4,10 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; #nullable disable -public class NorthwindBulkUpdatesNpgsqlTest( - NorthwindBulkUpdatesNpgsqlFixture fixture, +public class NorthwindBulkUpdatesGaussDBTest( + NorthwindBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) - : NorthwindBulkUpdatesRelationalTestBase>(fixture, testOutputHelper) + : NorthwindBulkUpdatesRelationalTestBase>(fixture, testOutputHelper) { public override async Task Delete_Where_TagWith(bool async) { diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 0000000000..1d9914d539 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class TPCFiltersInheritanceBulkUpdatesGaussDBFixture : TPCInheritanceBulkUpdatesGaussDBFixture +{ + public override bool EnableFilters + => true; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs similarity index 96% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs index 02f702961c..0addbaaa72 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -1,9 +1,9 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class TPCFiltersInheritanceBulkUpdatesNpgsqlTest( - TPCFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, +public class TPCFiltersInheritanceBulkUpdatesGaussDBTest( + TPCFiltersInheritanceBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) - : TPCFiltersInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) + : TPCFiltersInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { public override async Task Delete_where_hierarchy(bool async) { @@ -136,7 +136,7 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ -@p='0' +@p='0' (DbType = Int16) UPDATE "Kiwi" AS k SET "FoundOn" = @p @@ -174,7 +174,7 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ @p='Kiwi' -@p0='0' +@p0='0' (DbType = Int16) UPDATE "Kiwi" AS k SET "Name" = @p, diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 0000000000..efda6420ef --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,10 @@ +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class TPCInheritanceBulkUpdatesGaussDBFixture : TPCInheritanceBulkUpdatesFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override bool UseGeneratedKeys + => false; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs similarity index 96% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs index d66fdad9a3..89da3ada06 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs @@ -1,9 +1,9 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class TPCInheritanceBulkUpdatesNpgsqlTest( - TPCInheritanceBulkUpdatesNpgsqlFixture fixture, +public class TPCInheritanceBulkUpdatesGaussDBTest( + TPCInheritanceBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) - : TPCInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) + : TPCInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { public override async Task Delete_where_hierarchy(bool async) { @@ -135,7 +135,7 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ -@p='0' +@p='0' (DbType = Int16) UPDATE "Kiwi" AS k SET "FoundOn" = @p @@ -172,7 +172,7 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ @p='Kiwi' -@p0='0' +@p0='0' (DbType = Int16) UPDATE "Kiwi" AS k SET "Name" = @p, diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 0000000000..677b5fa98b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class TPHFiltersInheritanceBulkUpdatesGaussDBFixture : TPHInheritanceBulkUpdatesGaussDBFixture +{ + public override bool EnableFilters + => true; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs similarity index 96% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs index f963c3044a..e755a5d1e7 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -1,10 +1,10 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class TPHFiltersInheritanceBulkUpdatesNpgsqlTest( - TPHFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, +public class TPHFiltersInheritanceBulkUpdatesGaussDBTest( + TPHFiltersInheritanceBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : FiltersInheritanceBulkUpdatesRelationalTestBase< - TPHFiltersInheritanceBulkUpdatesNpgsqlFixture>(fixture, testOutputHelper) + TPHFiltersInheritanceBulkUpdatesGaussDBFixture>(fixture, testOutputHelper) { public override async Task Delete_where_hierarchy(bool async) { @@ -173,7 +173,7 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ -@p='0' +@p='0' (DbType = Int16) UPDATE "Animals" AS a SET "FoundOn" = @p @@ -188,7 +188,7 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ @p='Kiwi' -@p0='0' +@p0='0' (DbType = Int16) UPDATE "Animals" AS a SET "Name" = @p, diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs similarity index 77% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs index d783d64d3a..d26af0e64b 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlFixture.cs @@ -2,10 +2,10 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class TPHInheritanceBulkUpdatesNpgsqlFixture : TPHInheritanceBulkUpdatesFixture +public class TPHInheritanceBulkUpdatesGaussDBFixture : TPHInheritanceBulkUpdatesFixture { protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs index 3b2254152b..d14d66d8f3 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPHInheritanceBulkUpdatesNpgsqlTest.cs @@ -1,9 +1,9 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class TPHInheritanceBulkUpdatesNpgsqlTest( - TPHInheritanceBulkUpdatesNpgsqlFixture fixture, +public class TPHInheritanceBulkUpdatesGaussDBTest( + TPHInheritanceBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) - : TPHInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) + : TPHInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { public override async Task Delete_where_hierarchy(bool async) { @@ -183,7 +183,7 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ -@p='0' +@p='0' (DbType = Int16) UPDATE "Animals" AS a SET "FoundOn" = @p @@ -198,7 +198,7 @@ public override async Task Update_base_and_derived_types(bool async) AssertExecuteUpdateSql( """ @p='Kiwi' -@p0='0' +@p0='0' (DbType = Int16) UPDATE "Animals" AS a SET "Name" = @p, diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 0000000000..bd854652e9 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class TPTFiltersInheritanceBulkUpdatesGaussDBFixture : TPTInheritanceBulkUpdatesGaussDBFixture +{ + public override bool EnableFilters + => true; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs index 9da3e5dfb2..4ef584344d 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -1,9 +1,9 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPTFiltersInheritanceBulkUpdatesSqlServerTest( - TPTFiltersInheritanceBulkUpdatesNpgsqlFixture fixture, + TPTFiltersInheritanceBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) - : TPTFiltersInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) + : TPTFiltersInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { public override async Task Delete_where_hierarchy(bool async) { @@ -155,7 +155,7 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ -@p='0' +@p='0' (DbType = Int16) UPDATE "Kiwi" AS k SET "FoundOn" = @p diff --git a/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 0000000000..051f87dace --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.BulkUpdates; + +public class TPTInheritanceBulkUpdatesGaussDBFixture : TPTInheritanceBulkUpdatesFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs index 5c1b8dcc02..ac63e0b522 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs @@ -1,9 +1,9 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; -public class TPTInheritanceBulkUpdatesNpgsqlTest( - TPTInheritanceBulkUpdatesNpgsqlFixture fixture, +public class TPTInheritanceBulkUpdatesGaussDBTest( + TPTInheritanceBulkUpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) - : TPTInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) + : TPTInheritanceBulkUpdatesTestBase(fixture, testOutputHelper) { public override async Task Delete_where_hierarchy(bool async) { @@ -140,7 +140,7 @@ public override async Task Update_derived_property_on_derived_type(bool async) AssertExecuteUpdateSql( """ -@p='0' +@p='0' (DbType = Int16) UPDATE "Kiwi" AS k SET "FoundOn" = @p diff --git a/test/EFCore.GaussDB.FunctionalTests/CommandInterceptionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/CommandInterceptionGaussDBTest.cs new file mode 100644 index 0000000000..920eb5dc7c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/CommandInterceptionGaussDBTest.cs @@ -0,0 +1,57 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class CommandInterceptionGaussDBTestBase(CommandInterceptionGaussDBTestBase.InterceptionGaussDBFixtureBase fixture) + : CommandInterceptionTestBase(fixture) +{ + public abstract class InterceptionGaussDBFixtureBase : InterceptionFixtureBase + { + protected override string StoreName + => "CommandInterception"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkGaussDB(), injectedInterceptors); + } + + public class CommandInterceptionGaussDBTest(CommandInterceptionGaussDBTest.InterceptionGaussDBFixture fixture) + : CommandInterceptionGaussDBTestBase(fixture), IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener + => false; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new GaussDBExecutionStrategy(d)); + return builder; + } + } + } + + public class CommandInterceptionWithDiagnosticsGaussDBTest( + CommandInterceptionWithDiagnosticsGaussDBTest.InterceptionGaussDBFixture fixture) + : CommandInterceptionGaussDBTestBase(fixture), IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener + => true; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new GaussDBExecutionStrategy(d)); + return builder; + } + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ComplexTypesTrackingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ComplexTypesTrackingGaussDBTest.cs new file mode 100644 index 0000000000..e03c465fac --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ComplexTypesTrackingGaussDBTest.cs @@ -0,0 +1,23 @@ +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingGaussDBTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingGaussDBTest(GaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class GaussDBFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/CompositeKeyEndToEndTest.cs b/test/EFCore.GaussDB.FunctionalTests/CompositeKeyEndToEndTest.cs new file mode 100644 index 0000000000..6981fd878c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/CompositeKeyEndToEndTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore; + +public class CompositeKeyEndToEndGaussDBTest(CompositeKeyEndToEndGaussDBTest.CompositeKeyEndToEndGaussDBFixture fixture) + : CompositeKeyEndToEndTestBase(fixture) +{ + public class CompositeKeyEndToEndGaussDBFixture : CompositeKeyEndToEndFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs b/test/EFCore.GaussDB.FunctionalTests/ComputedColumnTest.cs similarity index 88% rename from test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs rename to test/EFCore.GaussDB.FunctionalTests/ComputedColumnTest.cs index 46de11410b..79b6c85e95 100644 --- a/test/EFCore.PG.FunctionalTests/ComputedColumnTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/ComputedColumnTest.cs @@ -7,7 +7,7 @@ public class ComputedColumnTest : IAsyncLifetime public void Can_use_computed_columns() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using var context = new Context(serviceProvider, TestStore.Name); @@ -31,7 +31,7 @@ public void Can_use_computed_columns() public void Can_use_computed_columns_with_null_values() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using var context = new Context(serviceProvider, TestStore.Name); @@ -54,7 +54,7 @@ private class Context(IServiceProvider serviceProvider, string databaseName) : D protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()) + .UseGaussDB(GaussDBTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()) .UseInternalServiceProvider(_serviceProvider); protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -105,7 +105,7 @@ private class NullableContext(IServiceProvider serviceProvider, string databaseN protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()) + .UseGaussDB(GaussDBTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()) .UseInternalServiceProvider(serviceProvider); protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -120,7 +120,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public void Can_use_computed_columns_with_nullable_enum() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using var context = new NullableContext(serviceProvider, TestStore.Name); @@ -132,10 +132,10 @@ public void Can_use_computed_columns_with_nullable_enum() Assert.Equal(FlagEnum.AValue | FlagEnum.BValue, entity.CalculatedFlagEnum); } - protected NpgsqlTestStore TestStore { get; private set; } = null!; + protected GaussDBTestStore TestStore { get; private set; } = null!; public async Task InitializeAsync() - => TestStore = await NpgsqlTestStore.CreateInitializedAsync("ComputedColumnTest"); + => TestStore = await GaussDBTestStore.CreateInitializedAsync("ComputedColumnTest"); public async Task DisposeAsync() => await TestStore.DisposeAsync(); diff --git a/test/EFCore.GaussDB.FunctionalTests/ConcurrencyDetectorDisabledGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ConcurrencyDetectorDisabledGaussDBTest.cs new file mode 100644 index 0000000000..abf2df4401 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ConcurrencyDetectorDisabledGaussDBTest.cs @@ -0,0 +1,40 @@ +namespace Microsoft.EntityFrameworkCore; + +public class ConcurrencyDetectorDisabledGaussDBTest : ConcurrencyDetectorDisabledRelationalTestBase< + ConcurrencyDetectorDisabledGaussDBTest.ConcurrencyDetectorGaussDBFixture> +{ + public ConcurrencyDetectorDisabledGaussDBTest(ConcurrencyDetectorGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + 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()); + + protected override async Task ConcurrencyDetectorTest(Func> test) + { + await base.ConcurrencyDetectorTest(test); + + Assert.NotEmpty(Fixture.TestSqlLoggerFactory.SqlStatements); + } + + public class ConcurrencyDetectorGaussDBFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.EnableThreadSafetyChecks(enableChecks: false); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ConcurrencyDetectorEnabledGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ConcurrencyDetectorEnabledGaussDBTest.cs new file mode 100644 index 0000000000..9fab1e94db --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ConcurrencyDetectorEnabledGaussDBTest.cs @@ -0,0 +1,37 @@ +namespace Microsoft.EntityFrameworkCore; + +public class ConcurrencyDetectorEnabledGaussDBTest : ConcurrencyDetectorEnabledRelationalTestBase< + ConcurrencyDetectorEnabledGaussDBTest.ConcurrencyDetectorGaussDBFixture> +{ + public ConcurrencyDetectorEnabledGaussDBTest(ConcurrencyDetectorGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + 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()); + + protected override async Task ConcurrencyDetectorTest(Func> test) + { + await base.ConcurrencyDetectorTest(test); + + Assert.Empty(Fixture.TestSqlLoggerFactory.SqlStatements); + } + + public class ConcurrencyDetectorGaussDBFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ConferencePlannerGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ConferencePlannerGaussDBTest.cs new file mode 100644 index 0000000000..86c8035954 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ConferencePlannerGaussDBTest.cs @@ -0,0 +1,169 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.TestModels.ConferencePlanner; + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class ConferencePlannerGaussDBTest(ConferencePlannerGaussDBTest.ConferencePlannerGaussDBFixture fixture) + : ConferencePlannerTestBase(fixture) +{ + // Overridden to use UTC DateTimeOffsets + public override async Task SessionsController_Post() + => await ExecuteWithStrategyInTransactionAsync( + async context => + { + var track = context.Tracks.AsNoTracking().OrderBy(e => e.Id).First(); + + var controller = new SessionsController(context); + + var result = await controller.Post( + new Session + { + Abstract = "Pandas eat bamboo all dat.", + Title = "Pandas!", + // GaussDB customizations + StartTime = DateTimeOffset.UtcNow, + EndTime = DateTimeOffset.UtcNow.AddHours(1), + TrackId = track.Id + }); + + var newSession = context.Sessions.AsNoTracking().Single(e => e.Title == "Pandas!"); + + Assert.Equal(newSession.Id, result.Id); + Assert.Null(result.Speakers); + Assert.NotNull(result.Track); + Assert.Equal(track.Id, result.Track.Id); + }); + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class ConferencePlannerGaussDBFixture : ConferencePlannerFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + // 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 async Task SeedAsync(ApplicationDbContext context) + { + var attendees1 = new List + { + new() + { + EmailAddress = "sonicrainboom@sample.com", + FirstName = "Rainbow", + LastName = "Dash", + UserName = "RainbowDash" + }, + new() + { + EmailAddress = "solovely@sample.com", + FirstName = "Flutter", + LastName = "Shy", + UserName = "Fluttershy" + } + }; + + var attendees2 = new List + { + new() + { + EmailAddress = "applesforever@sample.com", + FirstName = "Apple", + LastName = "Jack", + UserName = "Applejack" + }, + new() + { + EmailAddress = "precious@sample.com", + FirstName = "Rarity", + LastName = "", + UserName = "Rarity" + } + }; + + var attendees3 = new List + { + new() + { + EmailAddress = "princess@sample.com", + FirstName = "Twilight", + LastName = "Sparkle", + UserName = "Princess" + }, + new() + { + EmailAddress = "pinkie@sample.com", + FirstName = "Pinkie", + LastName = "Pie", + UserName = "Pinks" + } + }; + + using var document = JsonDocument.Parse(ConferenceData); + + var tracks = new Dictionary(); + var speakers = new Dictionary(); + + var root = document.RootElement; + foreach (var dayJson in root.EnumerateArray()) + { + foreach (var roomJson in dayJson.GetProperty("rooms").EnumerateArray()) + { + var roomId = roomJson.GetProperty("id").GetInt32(); + if (!tracks.TryGetValue(roomId, out var track)) + { + track = new Track { Name = roomJson.GetProperty("name").GetString(), Sessions = new List() }; + + tracks[roomId] = track; + } + + foreach (var sessionJson in roomJson.GetProperty("sessions").EnumerateArray()) + { + var sessionSpeakers = new List(); + foreach (var speakerJson in sessionJson.GetProperty("speakers").EnumerateArray()) + { + var speakerId = speakerJson.GetProperty("id").GetGuid(); + if (!speakers.TryGetValue(speakerId, out var speaker)) + { + speaker = new Speaker { Name = speakerJson.GetProperty("name").GetString() }; + + speakers[speakerId] = speaker; + } + + sessionSpeakers.Add(speaker); + } + + var session = new Session + { + Title = sessionJson.GetProperty("title").GetString(), + Abstract = sessionJson.GetProperty("description").GetString(), + // GaussDB customizations + StartTime = sessionJson.GetProperty("startsAt").GetDateTime().ToUniversalTime(), + EndTime = sessionJson.GetProperty("endsAt").GetDateTime().ToUniversalTime() + }; + + session.SessionSpeakers = sessionSpeakers.Select( + s => new SessionSpeaker { Session = session, Speaker = s }).ToList(); + + var trackName = track.Name; + var attendees = trackName.Contains("1") ? attendees1 + : trackName.Contains("2") ? attendees2 + : trackName.Contains("3") ? attendees3 + : attendees1.Concat(attendees2).Concat(attendees3).ToList(); + + session.SessionAttendees = attendees.Select( + a => new SessionAttendee { Session = session, Attendee = a }).ToList(); + + track.Sessions.Add(session); + } + } + } + + context.AddRange(tracks.Values); + await context.SaveChangesAsync(); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ConnectionInterceptionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ConnectionInterceptionGaussDBTest.cs new file mode 100644 index 0000000000..63a4b574af --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ConnectionInterceptionGaussDBTest.cs @@ -0,0 +1,100 @@ +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class ConnectionInterceptionGaussDBTestBase(ConnectionInterceptionGaussDBTestBase.InterceptionGaussDBFixtureBase fixture) + : ConnectionInterceptionTestBase(fixture) +{ + [ConditionalTheory(Skip = "#2368")] + public override Task Intercept_connection_creation_passively(bool async) + => base.Intercept_connection_creation_passively(async); + + [ConditionalTheory(Skip = "#2368")] + public override Task Intercept_connection_creation_with_multiple_interceptors(bool async) + => base.Intercept_connection_creation_with_multiple_interceptors(async); + + [ConditionalTheory(Skip = "#2368")] + public override Task Intercept_connection_to_override_connection_after_creation(bool async) + => base.Intercept_connection_to_override_connection_after_creation(async); + + [ConditionalTheory(Skip = "#2368")] + public override Task Intercept_connection_to_override_creation(bool async) + => base.Intercept_connection_to_override_creation(async); + + public abstract class InterceptionGaussDBFixtureBase : InterceptionFixtureBase + { + protected override string StoreName + => "ConnectionInterception"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkGaussDB(), injectedInterceptors); + } + + protected override DbContextOptionsBuilder ConfigureProvider(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(); + + protected override BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder) + => new(optionsBuilder.UseGaussDB(new FakeDbConnection()).Options); + + public class FakeDbConnection : DbConnection + { + [AllowNull] + public override string ConnectionString { get; set; } + + public override string Database + => "Database"; + + public override string DataSource + => "DataSource"; + + public override string ServerVersion + => throw new NotImplementedException(); + + public override ConnectionState State + => ConnectionState.Closed; + + public override void ChangeDatabase(string databaseName) + => throw new NotImplementedException(); + + public override void Close() + => throw new NotImplementedException(); + + public override void Open() + => throw new NotImplementedException(); + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + => throw new NotImplementedException(); + + protected override DbCommand CreateDbCommand() + => throw new NotImplementedException(); + } + + public class ConnectionInterceptionGaussDBTest(ConnectionInterceptionGaussDBTest.InterceptionGaussDBFixture fixture) + : ConnectionInterceptionGaussDBTestBase(fixture), IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener + => false; + } + } + + public class ConnectionInterceptionWithDiagnosticsGaussDBTest( + ConnectionInterceptionWithDiagnosticsGaussDBTest.InterceptionGaussDBFixture fixture) + : ConnectionInterceptionGaussDBTestBase(fixture), + IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener + => true; + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ConnectionSpecificationTest.cs b/test/EFCore.GaussDB.FunctionalTests/ConnectionSpecificationTest.cs new file mode 100644 index 0000000000..d582c25647 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ConnectionSpecificationTest.cs @@ -0,0 +1,284 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable StringLiteralTypo +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class ConnectionSpecificationTest +{ + [Fact] + public async Task Can_specify_connection_string_in_OnConfiguring() + { + var serviceProvider = new ServiceCollection() + .AddDbContext() + .BuildServiceProvider(); + + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); + + Assert.True(await context.Customers.AnyAsync()); + } + + [Fact] + public async Task Can_specify_connection_string_in_OnConfiguring_with_default_service_provider() + { + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = new StringInOnConfiguringContext(); + + Assert.True(await context.Customers.AnyAsync()); + } + + private class StringInOnConfiguringContext : NorthwindContextBase + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(GaussDBTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); + } + + [Fact] + public async Task Can_specify_connection_in_OnConfiguring() + { + var serviceProvider = new ServiceCollection() + .AddScoped(_ => new GaussDBConnection(GaussDBTestStore.NorthwindConnectionString)) + .AddDbContext().BuildServiceProvider(); + + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); + + Assert.True(await context.Customers.AnyAsync()); + } + + [Fact] + public async Task Can_specify_connection_in_OnConfiguring_with_default_service_provider() + { + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = new ConnectionInOnConfiguringContext(new GaussDBConnection(GaussDBTestStore.NorthwindConnectionString)); + + Assert.True(await context.Customers.AnyAsync()); + } + + private class ConnectionInOnConfiguringContext(GaussDBConnection connection) : NorthwindContextBase + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(connection, b => b.ApplyConfiguration()); + + public override void Dispose() + { + connection.Dispose(); + base.Dispose(); + } + } + + // ReSharper disable once UnusedMember.Local + private class StringInConfigContext : NorthwindContextBase + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB("Database=Crunchie", b => b.ApplyConfiguration()); + } + + [Fact] + public void Throws_if_no_connection_found_in_config_without_UseGaussDB() + { + var serviceProvider = new ServiceCollection() + .AddDbContext().BuildServiceProvider(); + + using var context = serviceProvider.GetRequiredService(); + + Assert.Equal( + CoreStrings.NoProviderConfigured, + Assert.Throws(() => context.Customers.Any()).Message); + } + + [Fact] + public void Throws_if_no_config_without_UseGaussDB() + { + var serviceProvider = new ServiceCollection() + .AddDbContext().BuildServiceProvider(); + using var context = serviceProvider.GetRequiredService(); + + Assert.Equal( + CoreStrings.NoProviderConfigured, + Assert.Throws(() => context.Customers.Any()).Message); + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class NoUseGaussDBContext : NorthwindContextBase; + + [Fact] + public async Task Can_depend_on_DbContextOptions() + { + var serviceProvider = new ServiceCollection() + .AddScoped(_ => new GaussDBConnection(GaussDBTestStore.NorthwindConnectionString)) + .AddDbContext() + .BuildServiceProvider(); + + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); + + Assert.True(await context.Customers.AnyAsync()); + } + + [Fact] + public async Task Can_depend_on_DbContextOptions_with_default_service_provider() + { + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = new OptionsContext( + new DbContextOptions(), + new GaussDBConnection(GaussDBTestStore.NorthwindConnectionString)); + + Assert.True(await context.Customers.AnyAsync()); + } + + private class OptionsContext(DbContextOptions options, GaussDBConnection connection) + : NorthwindContextBase(options) + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + Assert.Same(options, optionsBuilder.Options); + + optionsBuilder.UseGaussDB(connection, b => b.ApplyConfiguration()); + + Assert.NotSame(options, optionsBuilder.Options); + } + + public override void Dispose() + { + connection.Dispose(); + base.Dispose(); + } + } + + [Fact] + public async Task Can_depend_on_non_generic_options_when_only_one_context() + { + var serviceProvider = new ServiceCollection() + .AddDbContext() + .BuildServiceProvider(); + + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = serviceProvider.GetRequiredService(); + + Assert.True(await context.Customers.AnyAsync()); + } + + [Fact] + public async Task Can_depend_on_non_generic_options_when_only_one_context_with_default_service_provider() + { + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + await using var context = new NonGenericOptionsContext(new DbContextOptions()); + + Assert.True(await context.Customers.AnyAsync()); + } + + private class NonGenericOptionsContext(DbContextOptions options) : NorthwindContextBase(options) + { + private readonly DbContextOptions _options = options; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + Assert.Same(_options, optionsBuilder.Options); + + optionsBuilder.UseGaussDB(GaussDBTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); + + Assert.NotSame(_options, optionsBuilder.Options); + } + } + + private class NorthwindContextBase : DbContext + { + protected NorthwindContextBase() + { + } + + protected NorthwindContextBase(DbContextOptions options) + : base(options) + { + } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.HasKey(c => c.CustomerId); + b.ToTable("Customers"); + }); + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class Customer + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public string CustomerId { get; set; } + + // ReSharper disable once UnusedMember.Local + public string CompanyName { get; set; } + + // ReSharper disable once UnusedMember.Local + public string Fax { get; set; } + } + + #region Added for GaussDB + + [Fact] + public async Task Can_create_admin_connection_with_data_source() + { + await using var dataSource = GaussDBDataSource.Create(GaussDBTestStore.NorthwindConnectionString); + + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB(dataSource, b => b.ApplyConfiguration()); + await using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var relationalConnection = context.GetService(); + await using var adminConnection = relationalConnection.CreateAdminConnection(); + + Assert.Equal("postgres", new GaussDBConnectionStringBuilder(adminConnection.ConnectionString).Database); + + await adminConnection.OpenAsync(CancellationToken.None); + } + + [Fact] + public async Task Can_create_admin_connection_with_connection_string() + { + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB(GaussDBTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); + await using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var relationalConnection = context.GetService(); + await using var adminConnection = relationalConnection.CreateAdminConnection(); + + Assert.Equal("postgres", new GaussDBConnectionStringBuilder(adminConnection.ConnectionString).Database); + + await adminConnection.OpenAsync(CancellationToken.None); + } + + [Fact] + public async Task Can_create_admin_connection_with_connection() + { + await using var connection = new GaussDBConnection(GaussDBTestStore.NorthwindConnectionString); + connection.Open(); + + await using var _ = await GaussDBTestStore.GetNorthwindStoreAsync(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB(connection, b => b.ApplyConfiguration()); + await using var context = new GeneralOptionsContext(optionsBuilder.Options); + + var relationalConnection = context.GetService(); + await using var adminConnection = relationalConnection.CreateAdminConnection(); + + Assert.Equal("postgres", new GaussDBConnectionStringBuilder(adminConnection.ConnectionString).Database); + + adminConnection.Open(); + } + + private class GeneralOptionsContext(DbContextOptions options) : NorthwindContextBase(options); + + #endregion +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ConvertToProviderTypesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ConvertToProviderTypesGaussDBTest.cs new file mode 100644 index 0000000000..ab936792e2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ConvertToProviderTypesGaussDBTest.cs @@ -0,0 +1,123 @@ +namespace Microsoft.EntityFrameworkCore; + +public class ConvertToProviderTypesGaussDBTest : ConvertToProviderTypesTestBase< + ConvertToProviderTypesGaussDBTest.ConvertToProviderTypesGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public ConvertToProviderTypesGaussDBTest(ConvertToProviderTypesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + 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); + // } + // } + + [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] + 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 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 Task Can_insert_and_read_back_object_backed_data_types() + => Task.CompletedTask; + + public class ConvertToProviderTypesGaussDBFixture : ConvertToProviderTypesFixtureBase + { + public override bool StrictEquality + => true; + + public override bool SupportsAnsi + => false; + + public override bool SupportsUnicodeToAnsiConversion + => false; + + public override bool SupportsLargeStringComparisons + => true; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + public override bool SupportsBinaryKeys + => true; + + public override bool SupportsDecimalComparisons + => true; + + public override DateTime DefaultDateTime + => new(); + + public override bool PreservesDateTimeKind + => false; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + + // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data + var objectBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); + objectBackedDataTypes[nameof(ObjectBackedDataTypes.DateTimeOffset)] + = new DateTimeOffset(new DateTime(), TimeSpan.Zero); + + var nullableBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); + nullableBackedDataTypes[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); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/CustomConvertersGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/CustomConvertersGaussDBTest.cs new file mode 100644 index 0000000000..c2a480c526 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/CustomConvertersGaussDBTest.cs @@ -0,0 +1,87 @@ +namespace Microsoft.EntityFrameworkCore; + +public class CustomConvertersGaussDBTest(CustomConvertersGaussDBTest.CustomConvertersGaussDBFixture fixture) + : CustomConvertersTestBase(fixture) +{ + // Disabled: GaussDB is case-sensitive + 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 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 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 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 Task Can_query_using_any_data_type_nullable_shadow() + => Task.CompletedTask; + + public override void Value_conversion_on_enum_collection_contains() + => Assert.Contains( + CoreStrings.TranslationFailed("").Substring(47), + Assert.Throws(() => base.Value_conversion_on_enum_collection_contains()).Message); + + public class CustomConvertersGaussDBFixture : CustomConvertersFixtureBase + { + public override bool StrictEquality + => true; + + public override bool SupportsAnsi + => false; + + public override bool SupportsUnicodeToAnsiConversion + => true; + + public override bool SupportsLargeStringComparisons + => true; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override bool SupportsBinaryKeys + => true; + + public override bool SupportsDecimalComparisons + => true; + + public override DateTime DefaultDateTime + => new(); + + public override bool PreservesDateTimeKind + => false; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.DateTime) + .HasColumnType("timestamp without time zone"); + + // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data + var objectBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); + objectBackedDataTypes[nameof(ObjectBackedDataTypes.DateTimeOffset)] + = new DateTimeOffset(new DateTime(), TimeSpan.Zero); + + var nullableBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); + nullableBackedDataTypes[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); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/DataAnnotationGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/DataAnnotationGaussDBTest.cs new file mode 100644 index 0000000000..cfc14eb780 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/DataAnnotationGaussDBTest.cs @@ -0,0 +1,29 @@ +namespace Microsoft.EntityFrameworkCore; + +public class DataAnnotationGaussDBTest(DataAnnotationGaussDBTest.DataAnnotationGaussDBFixture fixture) + : DataAnnotationRelationalTestBase(fixture) +{ + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + protected override TestHelpers TestHelpers + => GaussDBTestHelpers.Instance; + + public override Task StringLengthAttribute_throws_while_inserting_value_longer_than_max_length() + => Task.CompletedTask; // GaussDB does not support length + + public override Task TimestampAttribute_throws_if_value_in_database_changed() + => Task.CompletedTask; // GaussDB does not support length + + public override Task MaxLengthAttribute_throws_while_inserting_value_longer_than_max_length() + => Task.CompletedTask; // GaussDB does not support length + + public class DataAnnotationGaussDBFixture : DataAnnotationRelationalFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/DataBindingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/DataBindingGaussDBTest.cs new file mode 100644 index 0000000000..233fb4d29f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/DataBindingGaussDBTest.cs @@ -0,0 +1,3 @@ +namespace Microsoft.EntityFrameworkCore; + +public class DataBindingGaussDBTest(F1BytesGaussDBFixture fixture) : DataBindingTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs b/test/EFCore.GaussDB.FunctionalTests/DefaultValuesTest.cs similarity index 93% rename from test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs rename to test/EFCore.GaussDB.FunctionalTests/DefaultValuesTest.cs index 6b27f2acfb..3ab59b65a4 100644 --- a/test/EFCore.PG.FunctionalTests/DefaultValuesTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/DefaultValuesTest.cs @@ -7,11 +7,11 @@ namespace Microsoft.EntityFrameworkCore; public class DefaultValuesTest : IDisposable { private readonly IServiceProvider _serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); [Fact] - public void Can_use_Npgsql_default_values() + public void Can_use_GaussDB_default_values() { using (var context = new ChipsContext(_serviceProvider, "DefaultKettleChips")) { @@ -53,8 +53,8 @@ private class ChipsContext(IServiceProvider serviceProvider, string databaseName protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql( - NpgsqlTestStore.CreateConnectionString(_databaseName), + .UseGaussDB( + GaussDBTestStore.CreateConnectionString(_databaseName), o => o.SetPostgresVersion(TestEnvironment.PostgresVersion)) .UseInternalServiceProvider(_serviceProvider); diff --git a/test/EFCore.GaussDB.FunctionalTests/DesignTimeGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/DesignTimeGaussDBTest.cs new file mode 100644 index 0000000000..8e3ddd8bf7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/DesignTimeGaussDBTest.cs @@ -0,0 +1,16 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Design.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class DesignTimeGaussDBTest(DesignTimeGaussDBTest.DesignTimeGaussDBFixture fixture) + : DesignTimeTestBase(fixture) +{ + protected override Assembly ProviderAssembly + => typeof(GaussDBDesignTimeServices).Assembly; + + public class DesignTimeGaussDBFixture : DesignTimeFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/EFCore.GaussDB.FunctionalTests.csproj b/test/EFCore.GaussDB.FunctionalTests/EFCore.GaussDB.FunctionalTests.csproj new file mode 100644 index 0000000000..f3fbfaa6e1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/EFCore.GaussDB.FunctionalTests.csproj @@ -0,0 +1,32 @@ + + + + HuaweiCloud.EntityFrameworkCore.GaussDB.FunctionalTests + Microsoft.EntityFrameworkCore + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/EFCore.GaussDB.FunctionalTests/EntitySplittingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/EntitySplittingGaussDBTest.cs new file mode 100644 index 0000000000..fc07c3a4f7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/EntitySplittingGaussDBTest.cs @@ -0,0 +1,8 @@ +namespace Microsoft.EntityFrameworkCore; + +public class EntitySplittingGaussDBTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) + : EntitySplittingTestBase(fixture, testOutputHelper) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs b/test/EFCore.GaussDB.FunctionalTests/ExecutionStrategyTest.cs similarity index 88% rename from test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs rename to test/EFCore.GaussDB.FunctionalTests/ExecutionStrategyTest.cs index cfd2690145..bcb34b85b9 100644 --- a/test/EFCore.PG.FunctionalTests/ExecutionStrategyTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/ExecutionStrategyTest.cs @@ -1,6 +1,6 @@ using System.Data; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; // ReSharper disable MethodSupportsCancellation // ReSharper disable AccessToDisposedClosure @@ -73,25 +73,25 @@ public void Handles_commit_failure(bool realFailure) IsolationLevel.Serializable)); } - private void Test_commit_failure(bool realFailure, Action execute) + private void Test_commit_failure(bool realFailure, Action execute) { CleanContext(); using (var context = CreateContext()) { - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.CommitFailures.Enqueue([realFailure]); Fixture.TestSqlLoggerFactory.Clear(); context.Products.Add(new Product()); - execute(new TestNpgsqlRetryingExecutionStrategy(context), context); + execute(new TestGaussDBRetryingExecutionStrategy(context), context); context.ChangeTracker.AcceptAllChanges(); var retryMessage = "A transient exception occurred during execution. The operation will be retried after 0ms." + Environment.NewLine - + "Npgsql.PostgresException (0x80004005): XX000"; + + "GaussDB.PostgresException (0x80004005): XX000"; if (realFailure) { var logEntry = Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == CoreEventId.ExecutionStrategyRetrying); @@ -187,25 +187,25 @@ await Test_commit_failure_async( private async Task Test_commit_failure_async( bool realFailure, - Func execute) + Func execute) { CleanContext(); await using (var context = CreateContext()) { - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.CommitFailures.Enqueue([realFailure]); Fixture.TestSqlLoggerFactory.Clear(); context.Products.Add(new Product()); - await execute(new TestNpgsqlRetryingExecutionStrategy(context), context); + await execute(new TestGaussDBRetryingExecutionStrategy(context), context); context.ChangeTracker.AcceptAllChanges(); var retryMessage = "A transient exception occurred during execution. The operation will be retried after 0ms." + Environment.NewLine - + "Npgsql.PostgresException (0x80004005): XX000"; + + "GaussDB.PostgresException (0x80004005): XX000"; if (realFailure) { var logEntry = Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == CoreEventId.ExecutionStrategyRetrying); @@ -233,7 +233,7 @@ public void Handles_commit_failure_multiple_SaveChanges(bool realFailure) CleanContext(); using var context1 = CreateContext(); - var connection = (TestNpgsqlConnection)context1.GetService(); + var connection = (TestPostgisConnection)context1.GetService(); using (var context2 = CreateContext()) { @@ -242,7 +242,7 @@ public void Handles_commit_failure_multiple_SaveChanges(bool realFailure) context1.Products.Add(new Product()); context2.Products.Add(new Product()); - new TestNpgsqlRetryingExecutionStrategy(context1).ExecuteInTransaction( + new TestGaussDBRetryingExecutionStrategy(context1).ExecuteInTransaction( context1, c1 => { @@ -275,7 +275,7 @@ public async Task Retries_SaveChanges_on_execution_failure( await using (var context = CreateContext()) { - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.ExecutionFailures.Enqueue([null, realFailure]); @@ -302,7 +302,7 @@ public async Task Retries_SaveChanges_on_execution_failure( { if (externalStrategy) { - await new TestNpgsqlRetryingExecutionStrategy(context).ExecuteInTransactionAsync( + await new TestGaussDBRetryingExecutionStrategy(context).ExecuteInTransactionAsync( context, (c, ct) => c.SaveChangesAsync(false, ct), (_, _) => @@ -322,7 +322,7 @@ public async Task Retries_SaveChanges_on_execution_failure( { if (externalStrategy) { - new TestNpgsqlRetryingExecutionStrategy(context).ExecuteInTransaction( + new TestGaussDBRetryingExecutionStrategy(context).ExecuteInTransaction( context, c => c.SaveChanges(false), _ => @@ -384,7 +384,7 @@ public async Task Retries_query_on_execution_failure(bool externalStrategy, bool await using (var context = CreateContext()) { - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.ExecutionFailures.Enqueue([true]); @@ -395,7 +395,7 @@ public async Task Retries_query_on_execution_failure(bool externalStrategy, bool { if (externalStrategy) { - list = await new TestNpgsqlRetryingExecutionStrategy(context) + list = await new TestGaussDBRetryingExecutionStrategy(context) .ExecuteAsync(context, (c, ct) => c.Products.ToListAsync(ct), null); } else @@ -407,7 +407,7 @@ public async Task Retries_query_on_execution_failure(bool externalStrategy, bool { if (externalStrategy) { - list = new TestNpgsqlRetryingExecutionStrategy(context) + list = new TestGaussDBRetryingExecutionStrategy(context) .Execute(context, c => c.Products.ToList(), null); } else @@ -440,7 +440,7 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, await using (var context = CreateContext()) { - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.ExecutionFailures.Enqueue([true]); @@ -451,7 +451,7 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, { if (externalStrategy) { - list = await new TestNpgsqlRetryingExecutionStrategy(context) + list = await new TestGaussDBRetryingExecutionStrategy(context) .ExecuteAsync( context, (c, ct) => c.Set().FromSqlRaw( """ @@ -470,7 +470,7 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, { if (externalStrategy) { - list = new TestNpgsqlRetryingExecutionStrategy(context) + list = new TestGaussDBRetryingExecutionStrategy(context) .Execute( context, c => c.Set().FromSqlRaw( """ @@ -500,7 +500,7 @@ public async Task Retries_FromSqlRaw_on_execution_failure(bool externalStrategy, public async Task Retries_OpenConnection_on_execution_failure(bool externalStrategy, bool async) { await using var context = CreateContext(); - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.OpenFailures.Enqueue([true]); @@ -510,7 +510,7 @@ public async Task Retries_OpenConnection_on_execution_failure(bool externalStrat { if (externalStrategy) { - await new TestNpgsqlRetryingExecutionStrategy(context).ExecuteAsync( + await new TestGaussDBRetryingExecutionStrategy(context).ExecuteAsync( context, c => c.Database.OpenConnectionAsync()); } @@ -523,7 +523,7 @@ public async Task Retries_OpenConnection_on_execution_failure(bool externalStrat { if (externalStrategy) { - new TestNpgsqlRetryingExecutionStrategy(context).Execute( + new TestGaussDBRetryingExecutionStrategy(context).Execute( context, c => c.Database.OpenConnection()); } @@ -553,7 +553,7 @@ public async Task Retries_OpenConnection_on_execution_failure(bool externalStrat public async Task Retries_BeginTransaction_on_execution_failure(bool async) { await using var context = CreateContext(); - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.OpenFailures.Enqueue([true]); @@ -561,7 +561,7 @@ public async Task Retries_BeginTransaction_on_execution_failure(bool async) if (async) { - var transaction = await new TestNpgsqlRetryingExecutionStrategy(context).ExecuteAsync( + var transaction = await new TestGaussDBRetryingExecutionStrategy(context).ExecuteAsync( context, _ => context.Database.BeginTransactionAsync()); @@ -569,7 +569,7 @@ public async Task Retries_BeginTransaction_on_execution_failure(bool async) } else { - var transaction = new TestNpgsqlRetryingExecutionStrategy(context).Execute( + var transaction = new TestGaussDBRetryingExecutionStrategy(context).Execute( context, _ => context.Database.BeginTransaction()); @@ -588,7 +588,7 @@ public void Verification_is_retried_using_same_retry_limit() using (var context = CreateContext()) { - var connection = (TestNpgsqlConnection)context.GetService(); + var connection = (TestPostgisConnection)context.GetService(); connection.ExecutionFailures.Enqueue([true, null, true, true]); connection.CommitFailures.Enqueue([true, true, true, true]); @@ -596,7 +596,7 @@ public void Verification_is_retried_using_same_retry_limit() context.Products.Add(new Product()); Assert.Throws( () => - new TestNpgsqlRetryingExecutionStrategy(context, TimeSpan.FromMilliseconds(100)) + new TestGaussDBRetryingExecutionStrategy(context, TimeSpan.FromMilliseconds(100)) .ExecuteInTransaction( context, c => c.SaveChanges(false), @@ -651,23 +651,23 @@ public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; protected override Type ContextType { get; } = typeof(ExecutionStrategyContext); protected override IServiceCollection AddServices(IServiceCollection serviceCollection) => base.AddServices(serviceCollection) .AddSingleton() - .AddScoped() + .AddScoped() .AddSingleton(); public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { var options = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(options) + new GaussDBDbContextOptionsBuilder(options) .MaxBatchSize(1) - .ExecutionStrategy(d => new TestNpgsqlRetryingExecutionStrategy(d)); + .ExecutionStrategy(d => new TestGaussDBRetryingExecutionStrategy(d)); return options; } diff --git a/test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs b/test/EFCore.GaussDB.FunctionalTests/ExistingConnectionTest.cs similarity index 89% rename from test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs rename to test/EFCore.GaussDB.FunctionalTests/ExistingConnectionTest.cs index cd14721057..899acfbabb 100644 --- a/test/EFCore.PG.FunctionalTests/ExistingConnectionTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/ExistingConnectionTest.cs @@ -16,17 +16,17 @@ public async Task Can_use_an_existing_open_connection() private static async Task Can_use_an_existing_closed_connection_test(bool openConnection) { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); - await using (var store = await NpgsqlTestStore.GetNorthwindStoreAsync()) + await using (var store = await GaussDBTestStore.GetNorthwindStoreAsync()) { store.CloseConnection(); var openCount = 0; var closeCount = 0; - await using (var connection = new NpgsqlConnection(store.ConnectionString)) + await using (var connection = new GaussDBConnection(store.ConnectionString)) { if (openConnection) { @@ -66,17 +66,17 @@ private static async Task Can_use_an_existing_closed_connection_test(bool openCo } } - private class NorthwindContext(IServiceProvider serviceProvider, NpgsqlConnection connection) : DbContext + private class NorthwindContext(IServiceProvider serviceProvider, GaussDBConnection connection) : DbContext { private readonly IServiceProvider _serviceProvider = serviceProvider; - private readonly NpgsqlConnection _connection = connection; + private readonly GaussDBConnection _connection = connection; // ReSharper disable once UnusedAutoPropertyAccessor.Local public DbSet Customers { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(_connection) + .UseGaussDB(_connection) .UseInternalServiceProvider(_serviceProvider); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/test/EFCore.GaussDB.FunctionalTests/F1GaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/F1GaussDBFixture.cs new file mode 100644 index 0000000000..183b635d2b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/F1GaussDBFixture.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +namespace Microsoft.EntityFrameworkCore; + +public class F1BytesGaussDBFixture : F1GaussDBFixtureBase +{ + protected override void BuildModelExternal(ModelBuilder modelBuilder) + { + base.BuildModelExternal(modelBuilder); + + modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); + modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); + modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); + modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); + modelBuilder.Entity() + .OwnsOne( + s => s.Details, eb => + { + eb.Property("Version").IsRowVersion().HasConversion(new ArrayStructuralComparer()); + }); + } + + private class BytesToUIntConverter() : ValueConverter( + bytes => BitConverter.ToUInt32(bytes), + num => BitConverter.GetBytes(num), + mappingHints: null); +} + +public class F1GaussDBFixture : F1GaussDBFixtureBase +{ + protected override void BuildModelExternal(ModelBuilder modelBuilder) + { + base.BuildModelExternal(modelBuilder); + + // TODO: This is a hack to work around, remove in 8.0 after https://github.com/dotnet/efcore/pull/29401 + modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); + modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); + modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); + modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); + modelBuilder.Entity() + .OwnsOne( + s => s.Details, eb => + { + eb.Property("Version").IsRowVersion().HasConversion((ValueConverter?)null); + }); + } +} + +public abstract class F1GaussDBFixtureBase : F1RelationalFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override TestHelpers TestHelpers + => GaussDBTestHelpers.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/FieldMappingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/FieldMappingGaussDBTest.cs new file mode 100644 index 0000000000..5d06033951 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/FieldMappingGaussDBTest.cs @@ -0,0 +1,16 @@ +namespace Microsoft.EntityFrameworkCore; + +public class FieldMappingGaussDBTest(FieldMappingGaussDBTest.FieldMappingGaussDBFixture fixture) + : FieldMappingTestBase(fixture) +{ + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class FieldMappingGaussDBFixture : FieldMappingFixtureBase + { + protected override string StoreName { get; } = "FieldMapping"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/FindGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/FindGaussDBTest.cs new file mode 100644 index 0000000000..c0e9ee3b8e --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/FindGaussDBTest.cs @@ -0,0 +1,34 @@ +namespace Microsoft.EntityFrameworkCore; + +public abstract class FindGaussDBTest : FindTestBase +{ + protected FindGaussDBTest(FindGaussDBFixture fixture) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + } + + public class FindGaussDBTestSet(FindGaussDBFixture fixture) : FindGaussDBTest(fixture) + { + protected override TestFinder Finder { get; } = new FindViaSetFinder(); + } + + public class FindGaussDBTestContext(FindGaussDBFixture fixture) : FindGaussDBTest(fixture) + { + protected override TestFinder Finder { get; } = new FindViaContextFinder(); + } + + public class FindGaussDBTestNonGeneric(FindGaussDBFixture fixture) : FindGaussDBTest(fixture) + { + protected override TestFinder Finder { get; } = new FindViaNonGenericContextFinder(); + } + + public class FindGaussDBFixture : FindFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/GaussDBApiConsistencyTest.cs b/test/EFCore.GaussDB.FunctionalTests/GaussDBApiConsistencyTest.cs new file mode 100644 index 0000000000..df5b5500ae --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/GaussDBApiConsistencyTest.cs @@ -0,0 +1,92 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class GaussDBApiConsistencyTest(GaussDBApiConsistencyTest.GaussDBApiConsistencyFixture fixture) + : ApiConsistencyTestBase(fixture) +{ + protected override void AddServices(ServiceCollection serviceCollection) + => serviceCollection.AddEntityFrameworkGaussDB(); + + protected override Assembly TargetAssembly + => typeof(GaussDBRelationalConnection).Assembly; + + public class GaussDBApiConsistencyFixture : ApiConsistencyFixtureBase + { + public override HashSet FluentApiTypes { get; } = + [ + typeof(GaussDBDbContextOptionsBuilder), + typeof(GaussDBDbContextOptionsBuilderExtensions), + typeof(GaussDBMigrationBuilderExtensions), + typeof(GaussDBIndexBuilderExtensions), + typeof(GaussDBModelBuilderExtensions), + typeof(GaussDBPropertyBuilderExtensions), + typeof(GaussDBEntityTypeBuilderExtensions), + typeof(GaussDBServiceCollectionExtensions) + ]; + + public override HashSet UnmatchedMetadataMethods { get; } = + [ + typeof(GaussDBPropertyBuilderExtensions).GetMethod( + nameof(GaussDBPropertyBuilderExtensions.IsGeneratedTsVectorColumn), + [typeof(PropertyBuilder), typeof(string), typeof(string[])]) + ]; + + public override + Dictionary MetadataExtensionTypes { get; } + = new() + { + { + typeof(IReadOnlyModel), ( + typeof(GaussDBModelExtensions), + typeof(GaussDBModelExtensions), + typeof(GaussDBModelExtensions), + typeof(GaussDBModelBuilderExtensions), + null + ) + }, + { + typeof(IReadOnlyEntityType), ( + typeof(GaussDBEntityTypeExtensions), + typeof(GaussDBEntityTypeExtensions), + typeof(GaussDBEntityTypeExtensions), + typeof(GaussDBEntityTypeBuilderExtensions), + null + ) + }, + { + typeof(IReadOnlyProperty), ( + typeof(GaussDBPropertyExtensions), + typeof(GaussDBPropertyExtensions), + typeof(GaussDBPropertyExtensions), + typeof(GaussDBPropertyBuilderExtensions), + null + ) + }, + { + typeof(IReadOnlyIndex), ( + typeof(GaussDBIndexExtensions), + typeof(GaussDBIndexExtensions), + typeof(GaussDBIndexExtensions), + typeof(GaussDBIndexBuilderExtensions), + null + ) + } + }; + + public override HashSet MetadataMethodExceptions { get; } = + [ + typeof(GaussDBEntityTypeExtensions).GetRuntimeMethod( + nameof(GaussDBEntityTypeExtensions.SetStorageParameter), + [typeof(IConventionEntityType), typeof(string), typeof(object), typeof(bool)]) + ]; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/GaussDBComplianceTest.cs b/test/EFCore.GaussDB.FunctionalTests/GaussDBComplianceTest.cs new file mode 100644 index 0000000000..6c1cdbd195 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/GaussDBComplianceTest.cs @@ -0,0 +1,20 @@ +namespace Microsoft.EntityFrameworkCore; + +public class GaussDBComplianceTest : RelationalComplianceTestBase +{ + protected override ICollection IgnoredTestBases { get; } = new HashSet + { + // Not implemented + typeof(CompiledModelTestBase), typeof(CompiledModelRelationalTestBase), // #3087 + typeof(FromSqlSprocQueryTestBase<>), + typeof(UdfDbFunctionTestBase<>), + typeof(UpdateSqlGeneratorTestBase), + + // Disabled + typeof(GraphUpdatesTestBase<>), + typeof(ProxyGraphUpdatesTestBase<>), + typeof(OperatorsProceduralQueryTestBase), + }; + + protected override Assembly TargetAssembly { get; } = typeof(GaussDBComplianceTest).Assembly; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/GaussDBDatabaseCreatorTest.cs b/test/EFCore.GaussDB.FunctionalTests/GaussDBDatabaseCreatorTest.cs new file mode 100644 index 0000000000..aedc7cf214 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/GaussDBDatabaseCreatorTest.cs @@ -0,0 +1,649 @@ +using System.Data; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable HeapView.CanAvoidClosure +// ReSharper disable MethodHasAsyncOverload + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class GaussDBDatabaseCreatorExistsTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true, true, false)] + [InlineData(false, false, false)] + [InlineData(true, true, true)] + [InlineData(false, false, true)] + public async Task Returns_false_when_database_does_not_exist(bool async, bool ambientTransaction, bool useCanConnect) + { + await using var testDatabase = GaussDBTestStore.Create("NonExisting"); + await using var context = new BloggingContext(testDatabase); + var creator = GetDatabaseCreator(context); + + await context.Database.CreateExecutionStrategy().ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + if (useCanConnect) + { + Assert.False(async ? await creator.CanConnectAsync() : creator.CanConnect()); + } + else + { + Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); + } + } + }); + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + } + + [ConditionalTheory] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + public async Task Returns_true_when_database_exists(bool async, bool ambientTransaction, bool useCanConnect) + { + await using var testDatabase = await GaussDBTestStore.GetOrCreateInitializedAsync("ExistingBlogging"); + await using var context = new BloggingContext(testDatabase); + var creator = GetDatabaseCreator(context); + + await context.Database.CreateExecutionStrategy().ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + if (useCanConnect) + { + Assert.True(async ? await creator.CanConnectAsync() : creator.CanConnect()); + } + else + { + Assert.True(async ? await creator.ExistsAsync() : creator.Exists()); + } + } + }); + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + } +} + +public class GaussDBDatabaseCreatorEnsureDeletedTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true, true, true)] + [InlineData(false, false, true)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + public async Task Deletes_database(bool async, bool open, bool ambientTransaction) + { + await using var testDatabase = await GaussDBTestStore.CreateInitializedAsync("EnsureDeleteBlogging"); + if (!open) + { + testDatabase.CloseConnection(); + } + + await using var context = new BloggingContext(testDatabase); + var creator = GetDatabaseCreator(context); + + Assert.True(async ? await creator.ExistsAsync() : creator.Exists()); + + await GetExecutionStrategy(testDatabase).ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + if (async) + { + Assert.True(await context.Database.EnsureDeletedAsync()); + } + else + { + Assert.True(context.Database.EnsureDeleted()); + } + } + }); + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + + Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + private static async Task Noop_when_database_does_not_exist_test(bool async) + { + await using var testDatabase = GaussDBTestStore.Create("NonExisting"); + await using var context = new BloggingContext(testDatabase); + var creator = GetDatabaseCreator(context); + + Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); + + if (async) + { + Assert.False(await creator.EnsureDeletedAsync()); + } + else + { + Assert.False(creator.EnsureDeleted()); + } + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + + Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + } +} + +public class GaussDBDatabaseCreatorEnsureCreatedTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true, true)] + [InlineData(false, false)] + public Task Creates_schema_in_existing_database(bool async, bool ambientTransaction) + => Creates_physical_database_and_schema_test((true, async, ambientTransaction)); + + [ConditionalTheory] + [InlineData(true, false)] + [InlineData(false, true)] + public Task Creates_physical_database_and_schema(bool async, bool ambientTransaction) + => Creates_new_physical_database_and_schema_test(async, ambientTransaction); + + private static Task Creates_new_physical_database_and_schema_test(bool async, bool ambientTransaction) + => Creates_physical_database_and_schema_test((false, async, ambientTransaction)); + + private static async Task Creates_physical_database_and_schema_test( + (bool CreateDatabase, bool Async, bool ambientTransaction) options) + { + var (createDatabase, async, ambientTransaction) = options; + await using var testDatabase = GaussDBTestStore.Create("EnsureCreatedTest"); + await using var context = new BloggingContext(testDatabase); + if (createDatabase) + { + await testDatabase.InitializeAsync(null, (Func)null); + } + else + { + await testDatabase.DeleteDatabaseAsync(); + } + + var creator = GetDatabaseCreator(context); + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + + using (CreateTransactionScope(ambientTransaction)) + { + if (async) + { + Assert.True(await creator.EnsureCreatedAsync()); + } + else + { + Assert.True(creator.EnsureCreated()); + } + } + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + + if (testDatabase.ConnectionState != ConnectionState.Open) + { + await testDatabase.OpenConnectionAsync(); + } + + var tables = testDatabase.Query( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND NOT TABLE_NAME LIKE ANY ('{pg_%,sql_%}')") + .ToList(); + Assert.Single(tables); + Assert.Equal("Blogs", tables.Single()); + + var columns = testDatabase.Query( + "SELECT TABLE_NAME || '.' || COLUMN_NAME || ' (' || DATA_TYPE || ')' FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Blogs' ORDER BY TABLE_NAME, COLUMN_NAME") + .ToArray(); + Assert.Equal(14, columns.Length); + + Assert.Equal( + new[] { + "Blogs.AndChew (bytea)", + "Blogs.AndRow (bytea)", + "Blogs.Cheese (text)", + "Blogs.ErMilan (integer)", + "Blogs.Fuse (smallint)", + "Blogs.George (boolean)", + "Blogs.Key1 (text)", + "Blogs.Key2 (bytea)", + "Blogs.NotFigTime (timestamp with time zone)", + "Blogs.On (real)", + "Blogs.OrNothing (double precision)", + "Blogs.TheGu (uuid)", + "Blogs.ToEat (smallint)", + "Blogs.WayRound (bigint)" + }, + columns); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Noop_when_database_exists_and_has_schema(bool async) + { + await using var testDatabase = await GaussDBTestStore.CreateInitializedAsync("InitializedBlogging"); + await using var context = new BloggingContext(testDatabase); + context.Database.EnsureCreatedResiliently(); + + if (async) + { + Assert.False(await context.Database.EnsureCreatedResilientlyAsync()); + } + else + { + Assert.False(context.Database.EnsureCreatedResiliently()); + } + + Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); + } +} + +public class GaussDBDatabaseCreatorHasTablesTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Throws_when_database_does_not_exist(bool async) + { + await using var testDatabase = GaussDBTestStore.GetOrCreate("NonExisting"); + var databaseCreator = GetDatabaseCreator(testDatabase); + await databaseCreator.ExecutionStrategy.ExecuteAsync( + databaseCreator, + async creator => + { + var errorNumber = async + ? (await Assert.ThrowsAsync(() => creator.HasTablesAsyncBase())).SqlState + : Assert.Throws(() => creator.HasTablesBase()).SqlState; + + Assert.Equal(PostgresErrorCodes.InvalidCatalogName, errorNumber); + }); + } + + [ConditionalTheory] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Returns_false_when_database_exists_but_has_no_tables(bool async, bool ambientTransaction) + { + await using var testDatabase = await GaussDBTestStore.GetOrCreateInitializedAsync("Empty"); + var creator = GetDatabaseCreator(testDatabase); + + await GetExecutionStrategy(testDatabase).ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + Assert.False(async ? await creator.HasTablesAsyncBase() : creator.HasTablesBase()); + } + }); + } + + [ConditionalTheory] + [InlineData(true, true)] + [InlineData(false, false)] + public async Task Returns_true_when_database_exists_and_has_any_tables(bool async, bool ambientTransaction) + { + await using var testDatabase = await GaussDBTestStore.GetOrCreate("ExistingTables") + .InitializeGaussDBAsync(null, t => new BloggingContext(t), null); + var creator = GetDatabaseCreator(testDatabase); + + await GetExecutionStrategy(testDatabase).ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + Assert.True(async ? await creator.HasTablesAsyncBase() : creator.HasTablesBase()); + } + }); + } + + [ConditionalTheory] + [InlineData(true, false)] + [InlineData(false, true)] + [RequiresPostgis] + public async Task Returns_false_when_database_exists_and_has_only_postgis_tables(bool async, bool ambientTransaction) + { + await using var testDatabase = await GaussDBTestStore.GetOrCreateInitializedAsync("Empty"); + testDatabase.ExecuteNonQuery("CREATE EXTENSION IF NOT EXISTS postgis"); + + var creator = GetDatabaseCreator(testDatabase); + + await GetExecutionStrategy(testDatabase).ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + Assert.False(async ? await creator.HasTablesAsyncBase() : creator.HasTablesBase()); + } + }); + } +} + +public class GaussDBDatabaseCreatorDeleteTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true, true)] + [InlineData(false, false)] + public static async Task Deletes_database(bool async, bool ambientTransaction) + { + await using var testDatabase = await GaussDBTestStore.CreateInitializedAsync("DeleteBlogging"); + testDatabase.CloseConnection(); + + var creator = GetDatabaseCreator(testDatabase); + + Assert.True(async ? await creator.ExistsAsync() : creator.Exists()); + + using (CreateTransactionScope(ambientTransaction)) + { + if (async) + { + await creator.DeleteAsync(); + } + else + { + creator.Delete(); + } + } + + Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Throws_when_database_does_not_exist(bool async) + { + await using var testDatabase = GaussDBTestStore.GetOrCreate("NonExistingBlogging"); + var creator = GetDatabaseCreator(testDatabase); + + if (async) + { + await Assert.ThrowsAsync(() => creator.DeleteAsync()); + } + else + { + Assert.Throws(() => creator.Delete()); + } + } +} + +public class GaussDBDatabaseCreatorCreateTablesTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true, true)] + [InlineData(false, false)] + public async Task Creates_schema_in_existing_database_test(bool async, bool ambientTransaction) + { + await using var testDatabase = await GaussDBTestStore.GetOrCreateInitializedAsync("ExistingBlogging" + (async ? "Async" : "")); + await using var context = new BloggingContext(testDatabase); + var creator = GetDatabaseCreator(context); + + using (CreateTransactionScope(ambientTransaction)) + { + if (async) + { + await creator.CreateTablesAsync(); + } + else + { + creator.CreateTables(); + } + } + + if (testDatabase.ConnectionState != ConnectionState.Open) + { + await testDatabase.OpenConnectionAsync(); + } + + var tables = (await testDatabase.QueryAsync( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND NOT TABLE_NAME LIKE ANY ('{pg_%,sql_%}')")) + .ToList(); + Assert.Single(tables); + Assert.Equal("Blogs", tables.Single()); + + var columns = (await testDatabase.QueryAsync( + "SELECT TABLE_NAME || '.' || COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Blogs'")).ToList(); + Assert.Equal(14, columns.Count); + Assert.Contains(columns, c => c == "Blogs.Key1"); + Assert.Contains(columns, c => c == "Blogs.Key2"); + Assert.Contains(columns, c => c == "Blogs.Cheese"); + Assert.Contains(columns, c => c == "Blogs.ErMilan"); + Assert.Contains(columns, c => c == "Blogs.George"); + Assert.Contains(columns, c => c == "Blogs.TheGu"); + Assert.Contains(columns, c => c == "Blogs.NotFigTime"); + Assert.Contains(columns, c => c == "Blogs.ToEat"); + Assert.Contains(columns, c => c == "Blogs.OrNothing"); + Assert.Contains(columns, c => c == "Blogs.Fuse"); + Assert.Contains(columns, c => c == "Blogs.WayRound"); + Assert.Contains(columns, c => c == "Blogs.On"); + Assert.Contains(columns, c => c == "Blogs.AndChew"); + Assert.Contains(columns, c => c == "Blogs.AndRow"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Throws_if_database_does_not_exist(bool async) + { + await using var testDatabase = GaussDBTestStore.GetOrCreate("NonExisting"); + var creator = GetDatabaseCreator(testDatabase); + + var errorNumber + = async + ? (await Assert.ThrowsAsync(() => creator.CreateTablesAsync())).SqlState + : Assert.Throws(() => creator.CreateTables()).SqlState; + + Assert.Equal(PostgresErrorCodes.InvalidCatalogName, errorNumber); + } + + [ConditionalFact] + public void GenerateCreateScript_works() + { + using var context = new BloggingContext("Data Source=foo"); + var script = context.Database.GenerateCreateScript(); + Assert.Equal( + """CREATE TABLE "Blogs" (""" + + _eol + + """ "Key1" text NOT NULL,""" + + _eol + + """ "Key2" bytea NOT NULL,""" + + _eol + + """ "Cheese" text,""" + + _eol + + """ "ErMilan" integer NOT NULL,""" + + _eol + + """ "George" boolean NOT NULL,""" + + _eol + + """ "TheGu" uuid NOT NULL,""" + + _eol + + """ "NotFigTime" timestamp with time zone NOT NULL,""" + + _eol + + """ "ToEat" smallint NOT NULL,""" + + _eol + + """ "OrNothing" double precision NOT NULL,""" + + _eol + + """ "Fuse" smallint NOT NULL,""" + + _eol + + """ "WayRound" bigint NOT NULL,""" + + _eol + + """ "On" real NOT NULL,""" + + _eol + + """ "AndChew" bytea,""" + + _eol + + """ "AndRow" bytea,""" + + _eol + + """ CONSTRAINT "PK_Blogs" PRIMARY KEY ("Key1", "Key2")""" + + _eol + + ");" + + _eol + + _eol + + _eol, + script); + } + + private static readonly string _eol = Environment.NewLine; +} + +public class GaussDBDatabaseCreatorCreateTest : GaussDBDatabaseCreatorTest +{ + [ConditionalTheory] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Creates_physical_database_but_not_tables(bool async, bool ambientTransaction) + { + await using var testDatabase = GaussDBTestStore.GetOrCreate("CreateTest"); + var creator = GetDatabaseCreator(testDatabase); + + creator.EnsureDeleted(); + + await GetExecutionStrategy(testDatabase).ExecuteAsync( + async () => + { + using (CreateTransactionScope(ambientTransaction)) + { + if (async) + { + await creator.CreateAsync(); + } + else + { + creator.Create(); + } + } + }); + + Assert.True(creator.Exists()); + + if (testDatabase.ConnectionState != ConnectionState.Open) + { + await testDatabase.OpenConnectionAsync(); + } + + Assert.Empty( + await testDatabase.QueryAsync( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND NOT TABLE_NAME LIKE ANY ('{pg_%,sql_%}')")); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Throws_if_database_already_exists(bool async) + { + await using var testDatabase = await GaussDBTestStore.GetOrCreateInitializedAsync("ExistingBlogging"); + var creator = GetDatabaseCreator(testDatabase); + + var ex = async + ? await Assert.ThrowsAsync(() => creator.CreateAsync()) + : Assert.Throws(() => creator.Create()); + Assert.Equal(PostgresErrorCodes.DuplicateDatabase, ex.SqlState); + } +} + +// When database creation/drop happens in parallel, there seem to be some deadlocks on the GaussDB side +// which make the tests run significantly slower. This makes all the test suites run in serial. +[Collection("GaussDBDatabaseCreatorTest")] +public class GaussDBDatabaseCreatorTest +{ + protected static IDisposable CreateTransactionScope(bool useTransaction) + => TestStore.CreateTransactionScope(useTransaction); + + protected static TestDatabaseCreator GetDatabaseCreator(GaussDBTestStore testStore) + => GetDatabaseCreator(testStore.ConnectionString); + + protected static TestDatabaseCreator GetDatabaseCreator(string connectionString) + => GetDatabaseCreator(new BloggingContext(connectionString)); + + protected static TestDatabaseCreator GetDatabaseCreator(BloggingContext context) + => (TestDatabaseCreator)context.GetService(); + + protected static IExecutionStrategy GetExecutionStrategy(GaussDBTestStore testStore) + => new BloggingContext(testStore).GetService().Create(); + + // ReSharper disable once ClassNeverInstantiated.Local + private class TestGaussDBExecutionStrategyFactory(ExecutionStrategyDependencies dependencies) + : GaussDBExecutionStrategyFactory(dependencies) + { + protected override IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) + => new NonRetryingExecutionStrategy(dependencies); + } + + private static IServiceProvider CreateServiceProvider() + => new ServiceCollection() + .AddEntityFrameworkGaussDB() + .AddScoped() + .AddScoped() + .BuildServiceProvider(); + + protected class BloggingContext(string connectionString) : DbContext + { + public BloggingContext(GaussDBTestStore testStore) + : this(testStore.ConnectionString) + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseGaussDB( + connectionString, b => b + .ApplyConfiguration() + .SetPostgresVersion(TestEnvironment.PostgresVersion)) + .UseInternalServiceProvider(CreateServiceProvider()); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.HasKey( + e => new { e.Key1, e.Key2 }); + b.Property(e => e.AndRow).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate(); + }); + + public DbSet Blogs { get; set; } + } + + public class Blog + { + public string Key1 { get; set; } + public byte[] Key2 { get; set; } + public string Cheese { get; set; } + public int ErMilan { get; set; } + public bool George { get; set; } + public Guid TheGu { get; set; } + public DateTime NotFigTime { get; set; } + public byte ToEat { get; set; } + public double OrNothing { get; set; } + public short Fuse { get; set; } + public long WayRound { get; set; } + public float On { get; set; } + public byte[] AndChew { get; set; } + public byte[] AndRow { get; set; } + } + + public class TestDatabaseCreator( + RelationalDatabaseCreatorDependencies dependencies, + IGaussDBRelationalConnection connection, + IRawSqlCommandBuilder rawSqlCommandBuilder, + IRelationalConnectionDiagnosticsLogger connectionLogger) + : GaussDBDatabaseCreator(dependencies, connection, rawSqlCommandBuilder, connectionLogger) + { + public bool HasTablesBase() + => HasTables(); + + public Task HasTablesAsyncBase(CancellationToken cancellationToken = default) + => HasTablesAsync(cancellationToken); + + public IExecutionStrategy ExecutionStrategy + => Dependencies.ExecutionStrategy; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/GaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/GaussDBFixture.cs new file mode 100644 index 0000000000..a078db8055 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/GaussDBFixture.cs @@ -0,0 +1,10 @@ +namespace Microsoft.EntityFrameworkCore; + +public class GaussDBFixture : ServiceProviderFixtureBase +{ + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/GaussDBServiceCollectionExtensionsTest.cs b/test/EFCore.GaussDB.FunctionalTests/GaussDBServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000000..6b3a63641f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/GaussDBServiceCollectionExtensionsTest.cs @@ -0,0 +1,3 @@ +namespace Microsoft.EntityFrameworkCore; + +public class GaussDBServiceCollectionExtensionsTest() : RelationalServiceCollectionExtensionsTestBase(GaussDBTestHelpers.Instance); diff --git a/test/EFCore.GaussDB.FunctionalTests/GaussDBValueGenerationScenariosTest.cs b/test/EFCore.GaussDB.FunctionalTests/GaussDBValueGenerationScenariosTest.cs new file mode 100644 index 0000000000..3e32177e65 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/GaussDBValueGenerationScenariosTest.cs @@ -0,0 +1,633 @@ +using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class GaussDBValueGenerationScenariosTest +{ + private static readonly string DatabaseName = "GaussDBValueGenerationScenariosTest"; + + [Fact] + public async Task Insert_with_sequence_id() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextSequence(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new Blog { Name = "One Unicorn" }, + new Blog { Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextSequence(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, blogs[0].Id); + Assert.Equal(2, blogs[1].Id); + } + } + + public class BlogContextSequence(string databaseName) : ContextBase(databaseName); + + [Fact] + public async Task Insert_with_sequence_HiLo() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextHiLo(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new Blog { Name = "One Unicorn" }, + new Blog { Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextHiLo(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(1, blogs[0].Id); + Assert.Equal(2, blogs[1].Id); + } + } + + public class BlogContextHiLo(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.UseHiLo(); + } + } + + [Fact] + public async Task Insert_with_default_value_from_sequence() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextDefaultValue(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new Blog { Name = "One Unicorn" }, + new Blog { Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextDefaultValue(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(1, blogs[1].Id); + } + + using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name)) + { + context.AddRange( + new Blog { Name = "One Unicorn" }, + new Blog { Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(1, blogs[1].Id); + Assert.Equal(2, blogs[2].Id); + Assert.Equal(3, blogs[3].Id); + } + } + + public class BlogContextDefaultValue(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .HasSequence("MySequence") + .HasMin(0) + .StartsAt(0); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValueSql("nextval('\"MySequence\"')"); + } + } + + public class BlogContextDefaultValueNoMigrations(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValue(); + } + } + + public class BlogWithStringKey + { + public string Id { get; set; } + public string Name { get; set; } + } + + [Fact] + public async Task Insert_with_key_default_value_from_sequence() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new Blog { Name = "One Unicorn" }, + new Blog { Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(77, blogs[0].Id); + Assert.Equal(78, blogs[1].Id); + } + } + + public class BlogContextKeyColumnWithDefaultValue(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .HasSequence("MySequence") + .StartsAt(77); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasDefaultValueSql("nextval('\"MySequence\"')") + .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); + } + } + + [ConditionalFact] + public async Task Insert_uint_to_Identity_column_using_value_converter() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithUIntKey { Name = "One Unicorn" }, new BlogWithUIntKey { Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) + { + var blogs = context.UnsignedBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal((uint)1, blogs[0].Id); + Assert.Equal((uint)2, blogs[1].Id); + } + } + + public class BlogContextUIntToIdentityUsingValueConverter(string databaseName) : ContextBase(databaseName) + { + public DbSet UnsignedBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .HasConversion(); + } + } + + public class BlogWithUIntKey + { + public uint Id { get; set; } + public string Name { get; set; } + } + + [ConditionalFact] + public async Task Insert_string_to_Identity_column_using_value_converter() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) + { + context.Database.EnsureCreatedResiliently(); + + context.AddRange( + new BlogWithStringKey { Name = "One Unicorn" }, new BlogWithStringKey { Name = "Two Unicorns" }); + + context.SaveChanges(); + } + + using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) + { + var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal("1", blogs[0].Id); + Assert.Equal("2", blogs[1].Id); + } + } + + public class BlogContextStringToIdentityUsingValueConverter(string databaseName) : ContextBase(databaseName) + { + public DbSet StringyBlogs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + Guid guid; + modelBuilder + .Entity() + .Property(e => e.Id) + .HasValueGenerator() + .HasConversion( + v => Guid.TryParse(v, out guid) + ? default + : int.Parse(v), + v => v.ToString()) + .ValueGeneratedOnAdd(); + } + } + + [Fact] + public async Task Insert_with_explicit_non_default_keys() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextNoKeyGeneration(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new Blog { Id = 66, Name = "One Unicorn" }, + new Blog { Id = 67, Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextNoKeyGeneration(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(66, blogs[0].Id); + Assert.Equal(67, blogs[1].Id); + } + } + + public class BlogContextNoKeyGeneration(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedNever(); + } + } + + [Fact] + public async Task Insert_with_explicit_with_default_keys() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new NullableKeyBlog { Id = 0, Name = "One Unicorn" }, + new NullableKeyBlog { Id = 1, Name = "Two Unicorns" }); + context.SaveChanges(); + } + + using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) + { + var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(0, blogs[0].Id); + Assert.Equal(1, blogs[1].Id); + } + } + + public class BlogContextNoKeyGenerationNullableKey(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedNever(); + } + } + + [Fact] + public async Task Insert_with_non_key_default_value() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) + { + context.Database.EnsureCreated(); + var blogs = new List + { + new() { Name = "One Unicorn" }, new() { Name = "Two Unicorns", CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0) } + }; + context.AddRange(blogs); + context.SaveChanges(); + + Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); + Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); + } + + using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); + + Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); + Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); + + blogs[0].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); + blogs[1].Name = "Zwo Unicorns"; + context.SaveChanges(); + } + + using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); + + Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); + Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[0].CreatedOn); + } + } + + public class BlogContextNonKeyDefaultValue(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .Property(e => e.CreatedOn) + .ValueGeneratedOnAdd() + .HasDefaultValueSql("now()"); + } + } + + [Fact] + public async Task Insert_with_non_key_default_value_readonly() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) + { + context.Database.EnsureCreated(); + context.AddRange( + new Blog { Name = "One Unicorn" }, + new Blog { Name = "Two Unicorns" }); + context.SaveChanges(); + Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn); + } + + DateTime dateTime0; + + using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + dateTime0 = blogs[0].CreatedOn; + + Assert.NotEqual(new DateTime(), dateTime0); + Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); + + blogs[0].Name = "One Pegasus"; + blogs[1].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); + + context.SaveChanges(); + } + + using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) + { + var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); + + Assert.Equal(dateTime0, blogs[0].CreatedOn); + Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[1].CreatedOn); + } + } + + public class BlogContextNonKeyReadOnlyDefaultValue(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .Property(e => e.CreatedOn) + .HasDefaultValueSql("now()") + .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); + } + } + + [Fact] + public async Task Insert_with_serial_non_id() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + int afterSave; + + using (var context = new BlogContextSequenceNonId(testStore.Name)) + { + context.Database.EnsureCreated(); + + var blog = context.Add(new Blog { Name = "One Unicorn" }).Entity; + var beforeSave = blog.OtherId; + context.SaveChanges(); + afterSave = blog.OtherId; + + Assert.NotEqual(beforeSave, afterSave); + } + + using (var context = new BlogContextSequenceNonId(testStore.Name)) + { + Assert.Equal(afterSave, context.Blogs.Single().OtherId); + } + } + + public class BlogContextSequenceNonId(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .Property(e => e.OtherId) + .ValueGeneratedOnAdd(); + } + } + + [Fact] + public async Task Insert_with_client_generated_GUID_key() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + Guid afterSave; + using (var context = new BlogContext(testStore.Name)) + { + context.Database.EnsureCreated(); + var blog = context.Add(new GuidBlog { Name = "One Unicorn" }).Entity; + var beforeSave = blog.Id; + context.SaveChanges(); + afterSave = blog.Id; + + Assert.Equal(beforeSave, afterSave); + } + + using (var context = new BlogContext(testStore.Name)) + { + Assert.Equal(afterSave, context.GuidBlogs.Single().Id); + } + } + + public class BlogContext(string databaseName) : ContextBase(databaseName); + + [Fact] + public async Task Insert_with_server_generated_GUID_key() + { + await using var testStore = await GaussDBTestStore.CreateInitializedAsync(DatabaseName); + + Guid afterSave; + using (var context = new BlogContextServerGuidKey(testStore.Name)) + { + context.Database.EnsureCreated(); + + var blog = context.Add( + new GuidBlog { Name = "One Unicorn" }).Entity; + var beforeSave = blog.Id; + var beforeSaveNotId = blog.NotId; + + Assert.Equal(default, beforeSave); + Assert.Equal(default, beforeSaveNotId); + + context.SaveChanges(); + + afterSave = blog.Id; + var afterSaveNotId = blog.NotId; + + Assert.NotEqual(default, afterSave); + Assert.NotEqual(default, afterSaveNotId); + Assert.NotEqual(beforeSave, afterSave); + Assert.NotEqual(beforeSaveNotId, afterSaveNotId); + } + + using (var context = new BlogContextServerGuidKey(testStore.Name)) + { + Assert.Equal(afterSave, context.GuidBlogs.Single().Id); + } + } + + public class BlogContextServerGuidKey(string databaseName) : ContextBase(databaseName) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresExtension("uuid-ossp"); + modelBuilder + .Entity( + eb => + { + eb.Property(e => e.Id) + .HasDefaultValueSql("uuid_generate_v4()"); + eb.Property(e => e.NotId) + .HasDefaultValueSql("uuid_generate_v4()"); + }); + } + } + + public class Blog + { + public int Id { get; set; } + public int OtherId { get; set; } + public string Name { get; set; } + public DateTime CreatedOn { get; set; } + } + + public class NullableKeyBlog + { + public int? Id { get; set; } + public string Name { get; set; } + public DateTime CreatedOn { get; set; } + } + + public class FullNameBlog + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string FullName { get; set; } + } + + public class GuidBlog + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid NotId { get; set; } + } + + public class ConcurrentBlog + { + public int Id { get; set; } + public string Name { get; set; } + public byte[] Timestamp { get; set; } + } + + public abstract class ContextBase(string databaseName) : DbContext + { + public DbSet Blogs { get; set; } + public DbSet NullableKeyBlogs { get; set; } + public DbSet FullNameBlogs { get; set; } + public DbSet GuidBlogs { get; set; } + public DbSet ConcurrentBlogs { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .EnableServiceProviderCaching(false) + .UseGaussDB( + GaussDBTestStore.CreateConnectionString(databaseName), + b => b.ApplyConfiguration()); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(b => b.CreatedOn).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(b => b.CreatedOn).HasColumnType("timestamp without time zone"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/JsonTypesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/JsonTypesGaussDBTest.cs new file mode 100644 index 0000000000..3fba273e0a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/JsonTypesGaussDBTest.cs @@ -0,0 +1,580 @@ +using System.Collections; +using System.Globalization; +using System.Numerics; +using NetTopologySuite; +using NetTopologySuite.Geometries; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore; + +public class JsonTypesGaussDBTest(NonSharedFixture fixture) : JsonTypesRelationalTestBase(fixture) +{ + #region Nested collections (unsupported) + + // The following tests are disabled because they use nested collections, which are not supported by EFCore.GaussDB (arrays of arrays aren't + // supported). + + 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 + + // 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 + + // 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 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); + + // 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; + + // 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 Task Can_read_write_collection_of_DateOnly_JSON_values_npgsql() + => Can_read_and_write_JSON_value>( + nameof(DateOnlyCollectionType.DateOnly), + [ + DateOnly.MinValue, + new(2023, 5, 29), + DateOnly.MaxValue + ], + """{"Prop":["-infinity","2023-05-29","infinity"]}""", + mappedCollection: true); + + protected class GaussDBDateOnlyCollectionType + { + public List DateOnly { get; set; } = null!; + } + + [ConditionalFact] + public override Task Can_read_write_collection_of_nullable_DateOnly_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(NullableDateOnlyCollectionType.DateOnly), + [ + DateOnly.MinValue, + new(2023, 5, 29), + DateOnly.MaxValue, + null + ], + """{"Prop":["-infinity","2023-05-29","infinity",null]}""", + mappedCollection: true); + + #endregion DateOnly + + #region DateTime + + // 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("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), 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_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; + + // 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("""{"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), + [ + DateTime.MinValue, + new(2023, 5, 29, 10, 52, 47), + DateTime.MaxValue + ], + expected, + mappedCollection: true); + + protected class GaussDBDateTimeCollectionType + { + 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), + [ + DateTime.MinValue, + null, + new(2023, 5, 29, 10, 52, 47), + DateTime.MaxValue + ], + """{"Prop":["-infinity",null,"2023-05-29T10:52:47","infinity"]}""", + mappedCollection: true); + + #endregion DateTime + + #region DateTimeOffset + + // 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.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); + + // 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; + + // 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 virtual Task Can_read_write_collection_of_DateTimeOffset_JSON_values_npgsql() + => Can_read_and_write_JSON_value>( + nameof(DateTimeOffsetCollectionType.DateTimeOffset), + [ + 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 virtual Task Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values_npgsql() + => Can_read_and_write_JSON_value>( + nameof(NullableDateTimeOffsetCollectionType.DateTimeOffset), + [ + 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); + + #endregion DateTimeOffset + + [ConditionalTheory] + [InlineData("12:34:56.123456+05:00", """{"Prop":"12:34:56.123456\u002B5"}""")] + public virtual void Can_read_write_timetz_JSON_values(string value, string json) + => Can_read_and_write_JSON_property_value( + b => b.HasColumnType("timetz"), + nameof(DateTimeOffsetType.DateTimeOffset), + DateTimeOffset.Parse(value), + json); + + [ConditionalTheory] + [InlineData(Mood.Happy, """{"Prop":"Happy"}""")] + [InlineData(Mood.Sad, """{"Prop":"Sad"}""")] + public virtual Task Can_read_write_pg_enum_JSON_values(Mood value, string json) + => Can_read_and_write_JSON_value( + nameof(EnumType.Mood), + value, + json); + + protected class EnumType + { + public Mood Mood { get; set; } + } + + public enum Mood + { + Happy, + Sad + } + + [ConditionalTheory] + [InlineData(new[] { 1, 2, 3 }, """{"Prop":[1,2,3]}""")] + [InlineData(new int[0], """{"Prop":[]}""")] + public virtual Task Can_read_write_array_JSON_values(int[] value, string json) + => Can_read_and_write_JSON_value( + nameof(ArrayType.Array), + value, + json, + mappedCollection: true); + + protected class ArrayType + { + public int[] Array { get; set; } = null!; + } + + [ConditionalFact] + 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.ThrowsAsync( + () => Can_read_and_write_JSON_value( + nameof(MultidimensionalArrayType.MultidimensionalArray), + new[,] { { 1, 2 }, { 3, 4 } }, + "")); + + protected class MultidimensionalArrayType + { + public int[,] MultidimensionalArray { get; set; } = null!; + } + + [ConditionalFact] + public virtual Task Can_read_write_BigInteger_JSON_values() + => Can_read_and_write_JSON_value( + nameof(BigIntegerType.BigInteger), + new BigInteger(ulong.MaxValue), + """{"Prop":"18446744073709551615"}"""); + + protected class BigIntegerType + { + public BigInteger BigInteger { get; set; } + } + + [ConditionalTheory] + [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 Task Can_read_write_BitArray_JSON_values(bool[] value, string json) + => Can_read_and_write_JSON_value( + nameof(BitArrayType.BitArray), + new BitArray(value), + json); + + protected class BitArrayType + { + public BitArray BitArray { get; set; } = null!; + } + + [ConditionalTheory] + [InlineData(1000, """{"Prop":"0/3E8"}""")] + [InlineData(0, """{"Prop":"0/0"}""")] + public virtual Task Can_read_write_LogSequenceNumber_JSON_values(ulong value, string json) + => Can_read_and_write_JSON_value( + nameof(LogSequenceNumberType.LogSequenceNumber), + new GaussDBLogSequenceNumber(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 GaussDBLogSequenceNumber LogSequenceNumber { get; set; } + } + + protected override ITestStoreFactory TestStoreFactory => GaussDBTestStoreFactory.Instance; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => serviceCollection.AddEntityFrameworkGaussDBNetTopologySuite(); + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + // 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.GaussDB from the ADO.NET layer. + new GaussDBDbContextOptionsBuilder(builder) + .MapEnum("mapped_enum", "test") + .UseNetTopologySuite(); + return builder; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/KeysWithConvertersGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/KeysWithConvertersGaussDBTest.cs new file mode 100644 index 0000000000..c305dfa028 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/KeysWithConvertersGaussDBTest.cs @@ -0,0 +1,15 @@ +namespace Microsoft.EntityFrameworkCore; + +public class KeysWithConvertersGaussDBTest(KeysWithConvertersGaussDBTest.KeysWithConvertersGaussDBFixture fixture) + : KeysWithConvertersTestBase< + KeysWithConvertersGaussDBTest.KeysWithConvertersGaussDBFixture>(fixture) +{ + public class KeysWithConvertersGaussDBFixture : KeysWithConvertersFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.UseGaussDB(b => b.MinBatchSize(1)); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/LazyLoadProxyGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/LazyLoadProxyGaussDBTest.cs new file mode 100644 index 0000000000..c5bc2c5592 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/LazyLoadProxyGaussDBTest.cs @@ -0,0 +1,1512 @@ +namespace Microsoft.EntityFrameworkCore; + +// ReSharper disable once UnusedMember.Global +public class LazyLoadProxyGaussDBTest : LazyLoadProxyRelationalTestBase +{ + public LazyLoadProxyGaussDBTest(LoadGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + [ConditionalFact] // Requires MARS + public override void Top_level_projection_track_entities_before_passing_to_client_method() { } + + [ConditionalTheory(Skip = "Possibly requires MARS, investigate")] + public override void Lazy_load_one_to_one_reference_with_recursive_property(EntityState state) + => base.Lazy_load_one_to_one_reference_with_recursive_property(state); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + protected override void RecordLog() + => Sql = Fixture.TestSqlLoggerFactory.Sql; + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private string Sql { get; set; } = null!; + + #region Expected JSON override + + // TODO: Tiny discrepancy in decimal representation (Charge: 1.0 instead of 1.00) + protected override string SerializedBlogs2 + => """ +{ + "$id": "1", + "$values": [ + { + "$id": "2", + "Id": 1, + "Writer": { + "$id": "3", + "FirstName": "firstNameWriter0", + "LastName": "lastNameWriter0", + "Alive": false, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "4", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "5", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "6", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "7", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "8", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "9", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "10", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Reader": { + "$id": "11", + "FirstName": "firstNameReader0", + "LastName": "lastNameReader0", + "Alive": false, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "12", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "13", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "14", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "15", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "16", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "17", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "18", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Host": { + "$id": "19", + "HostName": "127.0.0.1", + "Rating": 0, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "20", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "21", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "22", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "23", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "24", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "25", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "26", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "27", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "28", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "29", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "30", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "31", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "32", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "33", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + { + "$id": "34", + "Id": 2, + "Writer": { + "$id": "35", + "FirstName": "firstNameWriter1", + "LastName": "lastNameWriter1", + "Alive": false, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "36", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "37", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "38", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "39", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "40", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "41", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "42", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Reader": { + "$id": "43", + "FirstName": "firstNameReader1", + "LastName": "lastNameReader1", + "Alive": false, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "44", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "45", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "46", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "47", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "48", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "49", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "50", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Host": { + "$id": "51", + "HostName": "127.0.0.2", + "Rating": 0, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "52", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "53", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "54", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "55", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "56", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "57", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "58", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "59", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "60", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "61", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "62", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "63", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "64", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "65", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + { + "$id": "66", + "Id": 3, + "Writer": { + "$id": "67", + "FirstName": "firstNameWriter2", + "LastName": "lastNameWriter2", + "Alive": false, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "68", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "69", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "70", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "71", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "72", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "73", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "74", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Reader": { + "$id": "75", + "FirstName": "firstNameReader2", + "LastName": "lastNameReader2", + "Alive": false, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "76", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "77", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "78", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "79", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "80", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "81", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "82", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Host": { + "$id": "83", + "HostName": "127.0.0.3", + "Rating": 0, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "84", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "85", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "86", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "87", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "88", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "89", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "90", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + }, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "91", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "92", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "93", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "$id": "94", + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "$id": "95", + "Name": "M1", + "Rating": 7, + "Tag": { + "$id": "96", + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "$id": "97", + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + } + } + ] +} +"""; + + protected override string SerializedBlogs1 + => """ +[ + { + "Writer": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "FirstName": "firstNameWriter0", + "LastName": "lastNameWriter0", + "Alive": false + }, + "Reader": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "FirstName": "firstNameReader0", + "LastName": "lastNameReader0", + "Alive": false + }, + "Host": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "HostName": "127.0.0.1", + "Rating": 0.0 + }, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Id": 1 + }, + { + "Writer": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "FirstName": "firstNameWriter1", + "LastName": "lastNameWriter1", + "Alive": false + }, + "Reader": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "FirstName": "firstNameReader1", + "LastName": "lastNameReader1", + "Alive": false + }, + "Host": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "HostName": "127.0.0.2", + "Rating": 0.0 + }, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Id": 2 + }, + { + "Writer": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "FirstName": "firstNameWriter2", + "LastName": "lastNameWriter2", + "Alive": false + }, + "Reader": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "FirstName": "firstNameReader2", + "LastName": "lastNameReader2", + "Alive": false + }, + "Host": { + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "HostName": "127.0.0.3", + "Rating": 0.0 + }, + "Culture": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Milk": { + "Species": "S1", + "Subspecies": null, + "Rating": 8, + "Validation": false, + "Manufacturer": { + "Name": "M1", + "Rating": 7, + "Tag": { + "Text": "Ta2" + }, + "Tog": { + "Text": "To2" + } + }, + "License": { + "Title": "Ti1", + "Charge": 1.0, + "Tag": { + "Text": "Ta1" + }, + "Tog": { + "Text": "To1" + } + } + }, + "Id": 3 + } +] +"""; + + #endregion Expected JSON override + + public class LoadGaussDBFixture : LoadRelationalFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(q => q.Birthday).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(q => q.Birthday).HasColumnType("timestamp without time zone"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/LoadGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/LoadGaussDBTest.cs new file mode 100644 index 0000000000..c84e971f65 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/LoadGaussDBTest.cs @@ -0,0 +1,29 @@ +namespace Microsoft.EntityFrameworkCore; + +// ReSharper disable once UnusedMember.Global +public class LoadGaussDBTest : LoadTestBase +{ + public LoadGaussDBTest(LoadGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + protected override void RecordLog() + => Sql = Fixture.TestSqlLoggerFactory.Sql; + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private string Sql { get; set; } = null!; + + public class LoadGaussDBFixture : LoadFixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/LoggingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/LoggingGaussDBTest.cs new file mode 100644 index 0000000000..050f854804 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/LoggingGaussDBTest.cs @@ -0,0 +1,76 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class LoggingGaussDBTest : LoggingRelationalTestBase +{ + [Fact] + public void Logs_context_initialization_admin_database() + => Assert.Equal( + ExpectedMessage($"AdminDatabase=foo {DefaultOptions}"), + ActualMessage(s => CreateOptionsBuilder(s, b => ((GaussDBDbContextOptionsBuilder)b).UseAdminDatabase("foo")))); + + [Fact] + public void Logs_context_initialization_postgres_version() + => Assert.Equal( + ExpectedMessage($"PostgresVersion=10.7 {DefaultOptions}"), + ActualMessage(s => CreateOptionsBuilder(s, b => ((GaussDBDbContextOptionsBuilder)b).SetPostgresVersion(Version.Parse("10.7"))))); + +#pragma warning disable CS0618 // Authentication APIs on GaussDBDbContextOptionsBuilder are obsolete + [Fact] + public void Logs_context_initialization_provide_client_certificates_callback() + => Assert.Equal( + ExpectedMessage($"ProvideClientCertificatesCallback {DefaultOptions}"), + ActualMessage( + s => CreateOptionsBuilder( + s, b => ((GaussDBDbContextOptionsBuilder)b).ProvideClientCertificatesCallback(_ => { })))); + + [Fact] + public void Logs_context_initialization_provide_password_callback() + => Assert.Equal( + ExpectedMessage($"ProvidePasswordCallback {DefaultOptions}"), + ActualMessage( + s => CreateOptionsBuilder( + s, b => ((GaussDBDbContextOptionsBuilder)b).ProvidePasswordCallback((_, _, _, _) => "password")))); + + [Fact] + public void Logs_context_initialization_remote_certificate_validation_callback() + => Assert.Equal( + ExpectedMessage($"RemoteCertificateValidationCallback {DefaultOptions}"), + ActualMessage( + s => CreateOptionsBuilder( + s, + b => ((GaussDBDbContextOptionsBuilder)b).RemoteCertificateValidationCallback((_, _, _, _) => true)))); +#pragma warning restore CS0618 // Authentication APIs on GaussDBDbContextOptionsBuilder are obsolete + + [Fact] + public void Logs_context_initialization_reverse_null_ordering() + => Assert.Equal( + ExpectedMessage($"ReverseNullOrdering {DefaultOptions}"), + ActualMessage(s => CreateOptionsBuilder(s, b => ((GaussDBDbContextOptionsBuilder)b).ReverseNullOrdering()))); + + [Fact] + public void Logs_context_initialization_user_range_definitions() + => Assert.Equal( + ExpectedMessage($"UserRangeDefinitions=[{typeof(int)}=>int4range] " + DefaultOptions), + ActualMessage(s => CreateOptionsBuilder(s, b => ((GaussDBDbContextOptionsBuilder)b).MapRange("int4range")))); + + protected override DbContextOptionsBuilder CreateOptionsBuilder( + IServiceCollection services, + Action> relationalAction) + => new DbContextOptionsBuilder() + .UseInternalServiceProvider(services.AddEntityFrameworkGaussDB().BuildServiceProvider()) + .UseGaussDB("Data Source=LoggingGaussDBTest.db", relationalAction); + + protected override TestLogger CreateTestLogger() + => new TestLogger(); + + protected override string ProviderName + => "HuaweiCloud.EntityFrameworkCore.GaussDB"; + + protected override string ProviderVersion + => typeof(GaussDBOptionsExtension).Assembly + .GetCustomAttribute()?.InformationalVersion!; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ManyToManyFieldsLoadGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ManyToManyFieldsLoadGaussDBTest.cs new file mode 100644 index 0000000000..d4e054756a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ManyToManyFieldsLoadGaussDBTest.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyFieldsModel; + +namespace Microsoft.EntityFrameworkCore; + +public class ManyToManyFieldsLoadGaussDBTest(ManyToManyFieldsLoadGaussDBTest.ManyToManyFieldsLoadGaussDBFixture fixture) + : ManyToManyFieldsLoadTestBase(fixture) +{ + public class ManyToManyFieldsLoadGaussDBFixture : ManyToManyFieldsLoadFixtureBase, ITestSqlLoggerFactory + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity() + .Property(e => e.Key3) + .HasColumnType("timestamp without time zone"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasColumnType("timestamp without time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + modelBuilder + .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .IndexerProperty("Payload") + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValue("Generated"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ManyToManyLoadGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ManyToManyLoadGaussDBTest.cs new file mode 100644 index 0000000000..6f4de30b48 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ManyToManyLoadGaussDBTest.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; + +namespace Microsoft.EntityFrameworkCore; + +public class ManyToManyLoadGaussDBTest(ManyToManyLoadGaussDBTest.ManyToManyLoadGaussDBFixture fixture) + : ManyToManyLoadTestBase(fixture) +{ + public class ManyToManyLoadGaussDBFixture : ManyToManyLoadFixtureBase, ITestSqlLoggerFactory + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity() + .Property(e => e.Key3) + .HasColumnType("timestamp without time zone"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasColumnType("timestamp without time zone") + .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); + + modelBuilder + .Entity() + .Property(e => e.Key3) + .HasColumnType("timestamp without time zone") + .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasColumnType("timestamp without time zone") + .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); + + modelBuilder + .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .IndexerProperty("Payload") + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValue("Generated"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ManyToManyTrackingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ManyToManyTrackingGaussDBTest.cs new file mode 100644 index 0000000000..28edf78dde --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ManyToManyTrackingGaussDBTest.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; + +namespace Microsoft.EntityFrameworkCore; + +public class ManyToManyTrackingGaussDBTest(ManyToManyTrackingGaussDBTest.ManyToManyTrackingGaussDBFixture fixture) + : ManyToManyTrackingRelationalTestBase< + ManyToManyTrackingGaussDBTest.ManyToManyTrackingGaussDBFixture>(fixture) +{ + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class ManyToManyTrackingGaussDBFixture : ManyToManyTrackingRelationalFixture, ITestSqlLoggerFactory + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasColumnType("timestamp without time zone") + .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); + + modelBuilder + .SharedTypeEntity>("JoinOneToThreePayloadFullShared") + .IndexerProperty("Payload") + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasColumnType("timestamp without time zone") + .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); + + modelBuilder + .SharedTypeEntity>("UnidirectionalJoinOneToThreePayloadFullShared") + .IndexerProperty("Payload") + .HasDefaultValue("Generated"); + + modelBuilder + .Entity() + .Property(e => e.Payload) + .HasDefaultValue("Generated"); + + // Additional GaussDB-specific config (for timestamp without time zone) + modelBuilder + .Entity() + .Property(e => e.Key3) + .HasColumnType("timestamp without time zone"); + + modelBuilder.Entity() + .Property(e => e.Key3) + .HasColumnType("timestamp without time zone"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/MaterializationInterceptionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/MaterializationInterceptionGaussDBTest.cs new file mode 100644 index 0000000000..bbfcaa807f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/MaterializationInterceptionGaussDBTest.cs @@ -0,0 +1,21 @@ +namespace Microsoft.EntityFrameworkCore; + +public class MaterializationInterceptionGaussDBTest(NonSharedFixture fixture) : + MaterializationInterceptionTestBase(fixture) +{ + public class GaussDBLibraryContext(DbContextOptions options) : LibraryContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().OwnsMany(e => e.Settings); + + // #2548 + // modelBuilder.Entity().OwnsMany(e => e.Settings, b => b.ToJson()); + } + } + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs similarity index 82% rename from test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs index 3885e461b8..6bb36c515e 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Migrations/MigrationsInfrastructureNpgsqlTest.cs @@ -1,17 +1,17 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Migrations; #nullable disable -public class MigrationsInfrastructureNpgsqlTest(MigrationsInfrastructureNpgsqlTest.MigrationsInfrastructureNpgsqlFixture fixture) - : MigrationsInfrastructureTestBase(fixture) +public class MigrationsInfrastructureGaussDBTest(MigrationsInfrastructureGaussDBTest.MigrationsInfrastructureGaussDBFixture fixture) + : MigrationsInfrastructureTestBase(fixture) { public override void Can_get_active_provider() { base.Can_get_active_provider(); - Assert.Equal("Npgsql.EntityFrameworkCore.PostgreSQL", ActiveProvider); + Assert.Equal("HuaweiCloud.EntityFrameworkCore.GaussDB", ActiveProvider); } // See #3407 @@ -22,7 +22,7 @@ public override void Can_apply_two_migrations_in_transaction() 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() + // This tests uses Fixture.CreateEmptyContext(), which does not go through MigrationsInfrastructureGaussDBFixture.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)] @@ -57,7 +57,7 @@ public async Task Empty_Migration_Creates_Database() new DbContextOptionsBuilder().EnableServiceProviderCaching(false)) .ConfigureWarnings(e => e.Log(RelationalEventId.PendingModelChangesWarning)).Options); - var creator = (NpgsqlDatabaseCreator)context.GetService(); + var creator = (GaussDBDatabaseCreator)context.GetService(); creator.RetryTimeout = TimeSpan.FromMinutes(10); await context.Database.MigrateAsync(); @@ -105,8 +105,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("ProductVersion", "2.2.4-servicing-10062") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + "GaussDB:ValueGenerationStrategy", + GaussDBValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity( "ModelSnapshot22.Blog", b => @@ -114,8 +114,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasAnnotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + "GaussDB:ValueGenerationStrategy", + GaussDBValueGenerationStrategy.IdentityByDefaultColumn); b.Property("Name"); @@ -130,8 +130,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasAnnotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + "GaussDB:ValueGenerationStrategy", + GaussDBValueGenerationStrategy.IdentityByDefaultColumn); b.Property("BlogId"); @@ -175,18 +175,18 @@ public override void Can_diff_against_2_1_ASP_NET_Identity_model() } protected override Task ExecuteSqlAsync(string value) - => ((NpgsqlTestStore)Fixture.TestStore).ExecuteNonQueryAsync(value); + => ((GaussDBTestStore)Fixture.TestStore).ExecuteNonQueryAsync(value); - public class MigrationsInfrastructureNpgsqlFixture : MigrationsInfrastructureFixtureBase + public class MigrationsInfrastructureGaussDBFixture : MigrationsInfrastructureFixtureBase { protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public override MigrationsContext CreateContext() { var options = AddOptions( TestStore.AddProviderOptions(new DbContextOptionsBuilder()) - .UseNpgsql( + .UseGaussDB( TestStore.ConnectionString, b => b.ApplyConfiguration() .SetPostgresVersion(TestEnvironment.PostgresVersion))) .UseInternalServiceProvider(ServiceProvider) @@ -217,7 +217,7 @@ public class Post public class BloggingContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(TestEnvironment.DefaultConnection); + => optionsBuilder.UseGaussDB(TestEnvironment.DefaultConnection); public DbSet Blogs { get; set; } } \ No newline at end of file diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs similarity index 92% rename from test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index 46a2d6122a..fda0908182 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -1,13 +1,13 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; namespace Microsoft.EntityFrameworkCore.Migrations; -public class MigrationsNpgsqlTest : MigrationsTestBase +public class MigrationsGaussDBTest : MigrationsTestBase { - public MigrationsNpgsqlTest(MigrationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public MigrationsGaussDBTest(MigrationsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); @@ -147,8 +147,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql( @@ -171,8 +171,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql( @@ -196,7 +196,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Equal(10, options.StartValue); Assert.Equal(2, options.IncrementBy); @@ -226,7 +226,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.SerialColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.SerialColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); Assert.Empty(model.Sequences); }); @@ -330,17 +330,17 @@ await Test( Assert.Collection( table.GetAnnotations() - .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) + .Where(a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)) .OrderBy(a => a.Name), annotation => { - Assert.Equal(NpgsqlAnnotationNames.StorageParameterPrefix + "fillfactor", annotation.Name); + Assert.Equal(GaussDBAnnotationNames.StorageParameterPrefix + "fillfactor", annotation.Name); // Storage parameter values always get scaffolded as strings (PG storage is simply 'name=value') Assert.Equal("70", annotation.Value); }, annotation => { - Assert.Equal(NpgsqlAnnotationNames.StorageParameterPrefix + "user_catalog_table", annotation.Name); + Assert.Equal(GaussDBAnnotationNames.StorageParameterPrefix + "user_catalog_table", annotation.Name); // Storage parameter values always get scaffolded as strings (PG storage is simply 'name=value') Assert.Equal("true", annotation.Value); }); @@ -746,8 +746,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY;"""); @@ -770,8 +770,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED ALWAYS AS IDENTITY;"""); @@ -801,7 +801,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Equal(5, options.StartValue); Assert.Equal(2, options.IncrementBy); @@ -847,8 +847,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); - Assert.Equal(NpgsqlValueGenerationStrategy.SerialColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.SerialColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql("""ALTER TABLE "People" ADD "SomeColumn" serial NOT NULL;"""); @@ -871,8 +871,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "SomeColumn"); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql("""ALTER TABLE "People" ADD "SomeColumn" integer GENERATED BY DEFAULT AS IDENTITY;"""); @@ -904,7 +904,7 @@ await Test( [Fact] public virtual async Task Add_column_with_huge_varchar() { - // PostgreSQL doesn't allow varchar(x) with x > 10485760, so we map this to text. + // GaussDB doesn't allow varchar(x) with x > 10485760, so we map this to text. // See #342 and https://www.postgresql.org/message-id/15790.1291824247%40sss.pgh.pa.us await Test( builder => builder.Entity( @@ -941,7 +941,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "Title"); - Assert.Equal("pglz", column[NpgsqlAnnotationNames.CompressionMethod]); + Assert.Equal("pglz", column[GaussDBAnnotationNames.CompressionMethod]); }); AssertSql("""ALTER TABLE "Blogs" ADD "Title" text COMPRESSION pglz;"""); @@ -1062,7 +1062,7 @@ public override async Task Alter_column_change_computed() public override async Task Alter_column_change_computed_recreates_indexes() { - // PostgreSQL does not support indexes on virtual generated columns, which this test requires + // GaussDB does not support indexes on virtual generated columns, which this test requires // (0A000: indexes on virtual generated columns are not supported). // So we override the test to use stored generated columns instead. await Test( @@ -1182,8 +1182,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql( @@ -1210,8 +1210,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" SET GENERATED ALWAYS;"""); @@ -1234,8 +1234,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql( @@ -1258,7 +1258,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Equal(10, options.StartValue); Assert.Equal(2, options.IncrementBy); @@ -1293,7 +1293,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Null(options.StartValue); Assert.Equal(1, options.IncrementBy); @@ -1328,7 +1328,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Equal(2, options.IncrementBy); Assert.Equal(1000, options.MaxValue); @@ -1359,7 +1359,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Equal(5, options.StartValue); // Restarting doesn't change the scaffolded start value Assert.Equal(1, options.IncrementBy); @@ -1388,8 +1388,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.SerialColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.SerialColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); Assert.Empty(model.Sequences); }); @@ -1418,8 +1418,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.SerialColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.SerialColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); Assert.Empty(model.Sequences); }); @@ -1447,9 +1447,9 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.SerialColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.SerialColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); Assert.Equal("bigint", column.StoreType); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); Assert.Empty(model.Sequences); }); @@ -1481,8 +1481,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql( @@ -1505,8 +1505,8 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityAlwaysColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql( @@ -1539,9 +1539,9 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.SerialColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.SerialColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); Assert.Equal("bigint", column.StoreType); - Assert.Null(column[NpgsqlAnnotationNames.IdentityOptions]); + Assert.Null(column[GaussDBAnnotationNames.IdentityOptions]); }); AssertSql("""ALTER TABLE "People" ALTER COLUMN "Id" TYPE bigint;"""); @@ -1563,7 +1563,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, column[NpgsqlAnnotationNames.ValueGenerationStrategy]); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, column[GaussDBAnnotationNames.ValueGenerationStrategy]); var options = IdentitySequenceOptionsData.Get(column); Assert.Equal(10, options.StartValue); // Restarting doesn't change the scaffolded start value }); @@ -1654,7 +1654,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "Title"); - Assert.Equal("pglz", column[NpgsqlAnnotationNames.CompressionMethod]); + Assert.Equal("pglz", column[GaussDBAnnotationNames.CompressionMethod]); }); AssertSql("""ALTER TABLE "Blogs" ALTER COLUMN "Title" SET COMPRESSION pglz"""); @@ -1676,7 +1676,7 @@ await Test( { var table = Assert.Single(model.Tables); var column = Assert.Single(table.Columns, c => c.Name == "Title"); - Assert.Null(column[NpgsqlAnnotationNames.CompressionMethod]); + Assert.Null(column[GaussDBAnnotationNames.CompressionMethod]); }); AssertSql("""ALTER TABLE "Blogs" ALTER COLUMN "Title" SET COMPRESSION default"""); @@ -1791,7 +1791,7 @@ await Test( builder => { }, builder => builder.Entity("People") .HasIndex("X", "Y", "Z") - .HasAnnotation(NpgsqlAnnotationNames.IndexSortOrder, new[] { SortOrder.Ascending, SortOrder.Descending }), + .HasAnnotation(GaussDBAnnotationNames.IndexSortOrder, new[] { SortOrder.Ascending, SortOrder.Descending }), model => { var table = Assert.Single(model.Tables); @@ -1839,7 +1839,7 @@ await Test( Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); // Scaffolding included/covered properties is currently blocked, see #2194 - var includedColumns = (string[])index[NpgsqlAnnotationNames.IndexInclude]!; + var includedColumns = (string[])index[GaussDBAnnotationNames.IndexInclude]!; Assert.Null(includedColumns); // if (TestEnvironment.PostgresVersion.AtLeast(11)) @@ -1886,7 +1886,7 @@ await Test( Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); // Scaffolding included/covered properties is currently blocked, see #2194 - var includedColumns = (string[])index[NpgsqlAnnotationNames.IndexInclude]!; + var includedColumns = (string[])index[GaussDBAnnotationNames.IndexInclude]!; Assert.Null(includedColumns); // if (TestEnvironment.PostgresVersion.AtLeast(11)) @@ -1931,7 +1931,7 @@ await Test( Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); // Scaffolding included/covered properties is currently blocked, see #2194 - var includedColumns = (string[])index[NpgsqlAnnotationNames.IndexInclude]!; + var includedColumns = (string[])index[GaussDBAnnotationNames.IndexInclude]!; Assert.Null(includedColumns); // if (TestEnvironment.PostgresVersion.AtLeast(11)) @@ -1980,7 +1980,7 @@ await Test( Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); // Scaffolding included/covered properties is currently blocked, see #2194 - var includedColumns = (string[])index[NpgsqlAnnotationNames.IndexInclude]!; + var includedColumns = (string[])index[GaussDBAnnotationNames.IndexInclude]!; Assert.Null(includedColumns); // if (TestEnvironment.PostgresVersion.AtLeast(11)) @@ -2035,7 +2035,7 @@ await Test( { var table = Assert.Single(model.Tables); var index = Assert.Single(table.Indexes); - Assert.Equal("hash", index[NpgsqlAnnotationNames.IndexMethod]); + Assert.Equal("hash", index[GaussDBAnnotationNames.IndexMethod]); }); AssertSql("""CREATE INDEX "IX_People_Age" ON "People" USING hash ("Age");"""); @@ -2059,7 +2059,7 @@ await Test( { var table = Assert.Single(model.Tables); var index = Assert.Single(table.Indexes); - Assert.Equal(new[] { "text_pattern_ops", null }, index[NpgsqlAnnotationNames.IndexOperators]); + Assert.Equal(new[] { "text_pattern_ops", null }, index[GaussDBAnnotationNames.IndexOperators]); }); AssertSql("""CREATE INDEX "IX_People_FirstName_LastName" ON "People" ("FirstName" text_pattern_ops, "LastName");"""); @@ -2103,7 +2103,7 @@ await Test( { 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("text_pattern_ops", Assert.Single((IReadOnlyList)index[GaussDBAnnotationNames.IndexOperators]!)); Assert.Equal("some_collation", Assert.Single((IReadOnlyList)index[RelationalAnnotationNames.Collation]!)); }); @@ -2131,7 +2131,7 @@ await Test( var index = Assert.Single(table.Indexes); Assert.Equal( new[] { NullSortOrder.NullsFirst, NullSortOrder.NullsLast, NullSortOrder.NullsLast }, - index[NpgsqlAnnotationNames.IndexNullSortOrder]); + index[GaussDBAnnotationNames.IndexNullSortOrder]); }); AssertSql( @@ -2206,11 +2206,11 @@ await Test( var table = Assert.Single(model.Tables); Assert.Null( - Assert.Single(table.Indexes, i => i.Name == "IX_NullsDistinct")[NpgsqlAnnotationNames.NullsDistinct]); + Assert.Single(table.Indexes, i => i.Name == "IX_NullsDistinct")[GaussDBAnnotationNames.NullsDistinct]); Assert.Equal( false, - Assert.Single(table.Indexes, i => i.Name == "IX_NullsNotDistinct")[NpgsqlAnnotationNames.NullsDistinct]); + Assert.Single(table.Indexes, i => i.Name == "IX_NullsNotDistinct")[GaussDBAnnotationNames.NullsDistinct]); }); AssertSql( @@ -2238,9 +2238,9 @@ await Test( var index = Assert.Single(table.Indexes); var storageParameter = Assert.Single( index.GetAnnotations(), - a => a.Name.StartsWith(NpgsqlAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)); + a => a.Name.StartsWith(GaussDBAnnotationNames.StorageParameterPrefix, StringComparison.Ordinal)); - Assert.Equal(NpgsqlAnnotationNames.StorageParameterPrefix + "fillfactor", storageParameter.Name); + Assert.Equal(GaussDBAnnotationNames.StorageParameterPrefix + "fillfactor", storageParameter.Name); // Storage parameter values always get scaffolded as strings (PG storage is simply 'name=value') Assert.Equal("70", storageParameter.Value); }); @@ -2673,7 +2673,7 @@ public override async Task Add_required_primitve_collection_with_custom_default_ AssertSql("""ALTER TABLE "Customers" ADD "Numbers" integer[] NOT NULL DEFAULT (ARRAY[3, 2, 1]);"""); } - #region PostgreSQL extensions + #region GaussDB extensions [Fact] public virtual async Task Ensure_postgres_extension() @@ -2688,7 +2688,7 @@ await Test( Assert.Equal("public", citext.Schema); }); - AssertSql("CREATE EXTENSION IF NOT EXISTS citext CASCADE;"); + AssertSql("CREATE EXTENSION IF NOT EXISTS citext;"); } [Fact] @@ -2714,12 +2714,12 @@ IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'some_schema') THEN END $EF$; """, // - @"CREATE EXTENSION IF NOT EXISTS citext SCHEMA some_schema CASCADE;"); + @"CREATE EXTENSION IF NOT EXISTS citext SCHEMA some_schema;"); } #endregion - #region PostgreSQL enums + #region GaussDB enums [Fact] public virtual async Task Create_enum() @@ -2866,7 +2866,7 @@ public virtual Task Alter_enum_change_label_not_supported() #endregion - #region PostgreSQL collation management + #region GaussDB collation management [Fact] public virtual async Task Create_collation() @@ -2876,7 +2876,7 @@ await Test( builder => builder.HasCollation("dummy", locale: "POSIX", provider: "libc"), model => { - var collation = Assert.Single(PostgresCollation.GetCollations(model)); + var collation = Assert.Single(GaussDBCollation.GetCollations(model)); Assert.Equal("dummy", collation.Name); Assert.Equal("libc", collation.Provider); @@ -2902,7 +2902,7 @@ await Test( builder => builder.HasCollation("some_collation", locale: "en-u-ks-level1", provider: "icu", deterministic: false), model => { - var collation = Assert.Single(PostgresCollation.GetCollations(model)); + var collation = Assert.Single(GaussDBCollation.GetCollations(model)); Assert.Equal("some_collation", collation.Name); Assert.Equal("icu", collation.Provider); @@ -2926,7 +2926,7 @@ public virtual async Task Drop_collation() await Test( builder => builder.HasCollation("dummy", locale: "POSIX", provider: "libc"), _ => { }, - model => Assert.Empty(PostgresCollation.GetCollations(model))); + model => Assert.Empty(GaussDBCollation.GetCollations(model))); AssertSql("""DROP COLLATION dummy;"""); } @@ -2937,9 +2937,9 @@ public virtual Task Alter_collation_throws() builder => builder.HasCollation("dummy", locale: "POSIX", provider: "libc"), builder => builder.HasCollation("dummy", locale: "C", provider: "libc")); - #endregion PostgreSQL collation management + #endregion GaussDB collation management - #region PostgreSQL full-text search + #region GaussDB full-text search [Fact] public virtual async Task Add_column_generated_tsvector_over_text() @@ -2947,7 +2947,7 @@ public virtual async Task Add_column_generated_tsvector_over_text() await Test( builder => builder.Entity("Blogs", e => e.Property("TextColumn").IsRequired()), _ => { }, - builder => builder.Entity("Blogs").Property("SearchColumn").IsGeneratedTsVectorColumn("english", "TextColumn"), + builder => builder.Entity("Blogs").Property("SearchColumn").IsGeneratedTsVectorColumn("english", "TextColumn"), model => { var table = Assert.Single(model.Tables); @@ -2964,7 +2964,7 @@ public virtual async Task Add_column_generated_tsvector_over_jsonb() await Test( builder => builder.Entity("People").Property("JsonbColumn").HasColumnType("jsonb").IsRequired(), _ => { }, - builder => builder.Entity("People").Property("SearchColumn") + builder => builder.Entity("People").Property("SearchColumn") .IsGeneratedTsVectorColumn("english", "JsonbColumn"), model => { @@ -2989,7 +2989,7 @@ await Test( builder.Entity("People").Property("OptionalJsonColumn").HasColumnType("json"); }, _ => { }, - builder => builder.Entity("People").Property("SearchColumn") + builder => builder.Entity("People").Property("SearchColumn") .IsGeneratedTsVectorColumn( "english", "RequiredTextColumn", "OptionalTextColumn", "RequiredJsonbColumn", "OptionalJsonColumn"), model => @@ -3013,9 +3013,9 @@ await Test( e.Property("Title").IsRequired(); e.Property("Description"); }), - builder => builder.Entity("Blogs").Property("TsVector") + builder => builder.Entity("Blogs").Property("TsVector") .IsGeneratedTsVectorColumn("german", "Title", "Description"), - builder => builder.Entity("Blogs").Property("TsVector") + builder => builder.Entity("Blogs").Property("TsVector") .IsGeneratedTsVectorColumn("english", "Title", "Description"), model => { @@ -3033,7 +3033,7 @@ await Test( """ALTER TABLE "Blogs" ADD "TsVector" tsvector GENERATED ALWAYS AS (to_tsvector('english', "Title" || ' ' || coalesce("Description", ''))) STORED;"""); } - #endregion PostgreSQL full-text search + #endregion GaussDB full-text search public override async Task Add_required_primitive_collection_to_existing_table() { @@ -3125,9 +3125,6 @@ public override async Task Create_table_with_complex_type_with_required_properti "MyComplex_Prop" text, "MyComplex_MyNestedComplex_Bar" timestamp with time zone, "MyComplex_MyNestedComplex_Foo" integer, - "MyComplex_Nested_Bar" timestamp with time zone, - "MyComplex_Nested_Foo" integer, - "NestedCollection" jsonb, CONSTRAINT "PK_Contacts" PRIMARY KEY ("Id") ); """); @@ -3168,28 +3165,28 @@ private static bool SupportsVirtualGeneratedColumns protected override string NonDefaultCollation => "POSIX"; - public class MigrationsNpgsqlFixture : MigrationsFixtureBase + public class MigrationsGaussDBFixture : MigrationsFixtureBase { protected override string StoreName - => nameof(MigrationsNpgsqlTest); + => nameof(MigrationsGaussDBTest); protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public override RelationalTestHelpers TestHelpers - => NpgsqlTestHelpers.Instance; + => GaussDBTestHelpers.Instance; protected override IServiceCollection AddServices(IServiceCollection serviceCollection) => base.AddServices(serviceCollection) - .AddScoped(); + .AddScoped(); public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { - new NpgsqlDbContextOptionsBuilder( + new GaussDBDbContextOptionsBuilder( base.AddOptions(builder) // Some tests create expression indexes, but these cannot be reverse-engineered. .ConfigureWarnings( - w => { w.Ignore(NpgsqlEfEventId.ExpressionIndexSkippedWarning); })) + w => { w.Ignore(GaussDBEfEventId.ExpressionIndexSkippedWarning); })) // Various migration operations PG-version sensitive, configure the context with the actual version // we're connecting to. .SetPostgresVersion(TestEnvironment.PostgresVersion); @@ -3201,5 +3198,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("GaussDB")]; } diff --git a/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs b/test/EFCore.GaussDB.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs similarity index 92% rename from test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs index 2cf1b21d9b..2d9a6965cd 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs @@ -1,17 +1,17 @@ using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations.Operations; namespace Microsoft.EntityFrameworkCore.Migrations; -public class NpgsqlMigrationsSqlGeneratorTest() : MigrationsSqlGeneratorTestBase( - NpgsqlTestHelpers.Instance, - new ServiceCollection().AddEntityFrameworkNpgsqlNetTopologySuite(), - NpgsqlTestHelpers.Instance.AddProviderOptions( +public class GaussDBMigrationsSqlGeneratorTest() : MigrationsSqlGeneratorTestBase( + GaussDBTestHelpers.Instance, + new ServiceCollection().AddEntityFrameworkGaussDBNetTopologySuite(), + GaussDBTestHelpers.Instance.AddProviderOptions( ((IRelationalDbContextOptionsBuilderInfrastructure) - new NpgsqlDbContextOptionsBuilder(new DbContextOptionsBuilder()).UseNetTopologySuite()) + new GaussDBDbContextOptionsBuilder(new DbContextOptionsBuilder()).UseNetTopologySuite()) .OptionsBuilder).Options) { #region Database @@ -19,7 +19,7 @@ public class NpgsqlMigrationsSqlGeneratorTest() : MigrationsSqlGeneratorTestBase [Fact] public virtual void CreateDatabaseOperation() { - Generate(new NpgsqlCreateDatabaseOperation { Name = "Northwind" }); + Generate(new GaussDBCreateDatabaseOperation { Name = "Northwind" }); AssertSql( """ @@ -32,7 +32,7 @@ public virtual void CreateDatabaseOperation() public virtual void CreateDatabaseOperation_with_collation() { Generate( - new NpgsqlCreateDatabaseOperation { Name = "Northwind", Collation = "POSIX" }); + new GaussDBCreateDatabaseOperation { Name = "Northwind", Collation = "POSIX" }); AssertSql( """ @@ -45,7 +45,7 @@ CREATE DATABASE "Northwind" [Fact] public virtual void CreateDatabaseOperation_with_template() { - Generate(new NpgsqlCreateDatabaseOperation { Name = "Northwind", Template = "MyTemplate" }); + Generate(new GaussDBCreateDatabaseOperation { Name = "Northwind", Template = "MyTemplate" }); AssertSql( """ @@ -58,7 +58,7 @@ CREATE DATABASE "Northwind" [Fact] public virtual void CreateDatabaseOperation_with_tablespace() { - Generate(new NpgsqlCreateDatabaseOperation { Name = "some_db", Tablespace = "MyTablespace" }); + Generate(new GaussDBCreateDatabaseOperation { Name = "some_db", Tablespace = "MyTablespace" }); AssertSql( """ @@ -497,15 +497,15 @@ public void Alter_column_change_serial_to_identity_idempotent(MigrationsSqlGener Table = "Person", Name = "Id", ClrType = typeof(int), - [NpgsqlAnnotationNames.ValueGenerationStrategy] = - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, + [GaussDBAnnotationNames.ValueGenerationStrategy] = + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, OldColumn = new AddColumnOperation { Table = "Person", Name = "Id", ClrType = typeof(int), - [NpgsqlAnnotationNames.ValueGenerationStrategy] = - NpgsqlValueGenerationStrategy.SerialColumn, + [GaussDBAnnotationNames.ValueGenerationStrategy] = + GaussDBValueGenerationStrategy.SerialColumn, } } ], @@ -599,7 +599,7 @@ public virtual void AddColumnOperation_serial_old_annotation_throws() ClrType = typeof(int), ColumnType = "int", IsNullable = false, - [NpgsqlAnnotationNames.ValueGeneratedOnAdd] = true + [GaussDBAnnotationNames.ValueGeneratedOnAdd] = true })); public override void InsertDataOperation_throws_for_unsupported_column_types() diff --git a/test/EFCore.GaussDB.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs b/test/EFCore.GaussDB.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs new file mode 100644 index 0000000000..a422ad4225 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs @@ -0,0 +1,77 @@ +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.ModelBuilding; + +public class GaussDBModelBuilderGenericTest : GaussDBModelBuilderTestBase +{ + public class GaussDBGenericNonRelationship(GaussDBModelBuilderFixture fixture) : GaussDBNonRelationship(fixture) + { + // GaussDB 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 GaussDBGenericComplexType(GaussDBModelBuilderFixture fixture) : GaussDBComplexType(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericComplexCollection(GaussDBModelBuilderFixture fixture) : GaussDBComplexCollection(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericInheritance(GaussDBModelBuilderFixture fixture) : GaussDBInheritance(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericOneToMany(GaussDBModelBuilderFixture fixture) : GaussDBOneToMany(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericManyToOne(GaussDBModelBuilderFixture fixture) : GaussDBManyToOne(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericOneToOne(GaussDBModelBuilderFixture fixture) : GaussDBOneToOne(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericManyToMany(GaussDBModelBuilderFixture fixture) : GaussDBManyToMany(fixture) + { + protected override TestModelBuilder CreateModelBuilder( + Action? configure) + => new GenericTestModelBuilder(Fixture, configure); + } + + public class GaussDBGenericOwnedTypes(GaussDBModelBuilderFixture fixture) : GaussDBOwnedTypes(fixture) + { + // GaussDB 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.GaussDB.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs b/test/EFCore.GaussDB.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs new file mode 100644 index 0000000000..47b48189ab --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs @@ -0,0 +1,37 @@ +namespace Microsoft.EntityFrameworkCore.ModelBuilding; + +public class GaussDBModelBuilderTestBase : RelationalModelBuilderTest +{ + public abstract class GaussDBNonRelationship(GaussDBModelBuilderFixture fixture) + : RelationalNonRelationshipTestBase(fixture), IClassFixture; + + public abstract class GaussDBComplexType(GaussDBModelBuilderFixture fixture) + : RelationalComplexTypeTestBase(fixture), IClassFixture; + + public abstract class GaussDBComplexCollection(GaussDBModelBuilderFixture fixture) + : RelationalComplexCollectionTestBase(fixture), IClassFixture; + + public abstract class GaussDBInheritance(GaussDBModelBuilderFixture fixture) + : RelationalInheritanceTestBase(fixture), IClassFixture; + + public abstract class GaussDBOneToMany(GaussDBModelBuilderFixture fixture) + : RelationalOneToManyTestBase(fixture), IClassFixture; + + public abstract class GaussDBManyToOne(GaussDBModelBuilderFixture fixture) + : RelationalManyToOneTestBase(fixture), IClassFixture; + + public abstract class GaussDBOneToOne(GaussDBModelBuilderFixture fixture) + : RelationalOneToOneTestBase(fixture), IClassFixture; + + public abstract class GaussDBManyToMany(GaussDBModelBuilderFixture fixture) + : RelationalManyToManyTestBase(fixture), IClassFixture; + + public abstract class GaussDBOwnedTypes(GaussDBModelBuilderFixture fixture) + : RelationalOwnedTypesTestBase(fixture), IClassFixture; + + public class GaussDBModelBuilderFixture : RelationalModelBuilderFixture + { + public override TestHelpers TestHelpers + => GaussDBTestHelpers.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ModelBuilding101GaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ModelBuilding101GaussDBTest.cs new file mode 100644 index 0000000000..960557dc13 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ModelBuilding101GaussDBTest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore; + +public class ModelBuilding101GaussDBTest : ModelBuilding101RelationalTestBase +{ + protected override DbContextOptionsBuilder ConfigureContext(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/MonsterFixupChangedChangingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/MonsterFixupChangedChangingGaussDBTest.cs new file mode 100644 index 0000000000..b0f682ebad --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/MonsterFixupChangedChangingGaussDBTest.cs @@ -0,0 +1,32 @@ +namespace Microsoft.EntityFrameworkCore; + +public class MonsterFixupChangedChangingGaussDBTest(MonsterFixupChangedChangingGaussDBTest.MonsterFixupChangedChangingGaussDBFixture fixture) + : MonsterFixupTestBase(fixture) +{ + public class MonsterFixupChangedChangingGaussDBFixture : MonsterFixupChangedChangingFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating( + ModelBuilder builder) + { + base.OnModelCreating(builder); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + foreach (var property in builder.Model.GetEntityTypes() + .SelectMany( + e => e.GetProperties().Where( + p => + p.ClrType == typeof(DateTime) || p.ClrType == typeof(DateTime?)))) + { + property.SetColumnType("timestamp without time zone"); + } + + builder.Entity().Property(e => e.MessageId).UseSerialColumn(); + builder.Entity().Property(e => e.PhotoId).UseSerialColumn(); + builder.Entity().Property(e => e.ReviewId).UseSerialColumn(); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/MusicStoreGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/MusicStoreGaussDBTest.cs new file mode 100644 index 0000000000..75edc44dc1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/MusicStoreGaussDBTest.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.TestModels.MusicStore; + +namespace Microsoft.EntityFrameworkCore; + +public class MusicStoreGaussDBTest(MusicStoreGaussDBTest.MusicStoreGaussDBFixture fixture) + : MusicStoreTestBase(fixture) +{ + public class MusicStoreGaussDBFixture : MusicStoreFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().Property(s => s.DateCreated).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(s => s.OrderDate).HasColumnType("timestamp without time zone"); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Northwind.sql b/test/EFCore.GaussDB.FunctionalTests/Northwind.sql similarity index 100% rename from test/EFCore.PG.FunctionalTests/Northwind.sql rename to test/EFCore.GaussDB.FunctionalTests/Northwind.sql diff --git a/test/EFCore.GaussDB.FunctionalTests/NotificationEntitiesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/NotificationEntitiesGaussDBTest.cs new file mode 100644 index 0000000000..56ec8bdf8a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/NotificationEntitiesGaussDBTest.cs @@ -0,0 +1,13 @@ +namespace Microsoft.EntityFrameworkCore; + +public class NotificationEntitiesGaussDBTest(NotificationEntitiesGaussDBTest.NotificationEntitiesGaussDBFixture fixture) + : NotificationEntitiesTestBase(fixture) +{ + public class NotificationEntitiesGaussDBFixture : NotificationEntitiesFixtureBase + { + protected override string StoreName { get; } = "NotificationEntities"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/OptimisticConcurrencyGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/OptimisticConcurrencyGaussDBTest.cs new file mode 100644 index 0000000000..eea69cb546 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/OptimisticConcurrencyGaussDBTest.cs @@ -0,0 +1,146 @@ +using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class OptimisticConcurrencyBytesGaussDBTest(F1BytesGaussDBFixture fixture) + : OptimisticConcurrencyGaussDBTestBase(fixture); + +// uint maps directly to xid, which is the PG type of the xmin column that we use as a row version. +public class OptimisticConcurrencyGaussDBTest(F1GaussDBFixture fixture) : OptimisticConcurrencyGaussDBTestBase(fixture); + +public abstract class OptimisticConcurrencyGaussDBTestBase(TFixture fixture) + : OptimisticConcurrencyRelationalTestBase(fixture) + where TFixture : F1RelationalFixture, new() +{ + [ConditionalFact] + public async Task Modifying_concurrency_token_only_is_noop() + { + await using var c = CreateF1Context(); + await c.Database.CreateExecutionStrategy().ExecuteAsync( + c, async context => + { + await using var transaction = context.Database.BeginTransaction(); + var driver = context.Drivers.Single(d => d.CarNumber == 1); + driver.Podiums = StorePodiums; + var firstVersion = context.Entry(driver).Property("Version").CurrentValue; + await context.SaveChangesAsync(); + + await using var innerContext = CreateF1Context(); + innerContext.Database.UseTransaction(transaction.GetDbTransaction()); + driver = innerContext.Drivers.Single(d => d.CarNumber == 1); + Assert.NotEqual(firstVersion, innerContext.Entry(driver).Property("Version").CurrentValue); + Assert.Equal(StorePodiums, driver.Podiums); + + var secondVersion = innerContext.Entry(driver).Property("Version").CurrentValue; + innerContext.Entry(driver).Property("Version").CurrentValue = firstVersion; + await innerContext.SaveChangesAsync(); + await using var validationContext = CreateF1Context(); + validationContext.Database.UseTransaction(transaction.GetDbTransaction()); + driver = validationContext.Drivers.Single(d => d.CarNumber == 1); + Assert.Equal(secondVersion, validationContext.Entry(driver).Property("Version").CurrentValue); + Assert.Equal(StorePodiums, driver.Podiums); + }); + } + + [ConditionalFact] + public async Task Database_concurrency_token_value_is_updated_for_all_sharing_entities() + { + await using var c = CreateF1Context(); + await c.Database.CreateExecutionStrategy().ExecuteAsync( + c, async context => + { + await using var transaction = context.Database.BeginTransaction(); + var sponsor = context.Set().Single(); + var sponsorEntry = c.Entry(sponsor); + var detailsEntry = sponsorEntry.Reference(s => s.Details).TargetEntry; + var sponsorVersion = sponsorEntry.Property("Version").CurrentValue; + var detailsVersion = detailsEntry.Property("Version").CurrentValue; + + Assert.Null(sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); + sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue = 1; + + sponsor.Name = "Telecom"; + + Assert.Equal(sponsorVersion, detailsVersion); + + await context.SaveChangesAsync(); + + var newSponsorVersion = sponsorEntry.Property("Version").CurrentValue; + var newDetailsVersion = detailsEntry.Property("Version").CurrentValue; + + Assert.Equal(newSponsorVersion, newDetailsVersion); + Assert.NotEqual(sponsorVersion, newSponsorVersion); + + Assert.Equal(1, sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); + Assert.Equal(1, detailsEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); + }); + } + + [ConditionalFact] + public async Task Original_concurrency_token_value_is_used_when_replacing_owned_instance() + { + await using var c = CreateF1Context(); + await c.Database.CreateExecutionStrategy().ExecuteAsync( + c, async context => + { + await using var transaction = context.Database.BeginTransaction(); + var sponsor = context.Set().Single(); + var sponsorEntry = c.Entry(sponsor); + var sponsorVersion = sponsorEntry.Property("Version").CurrentValue; + + Assert.Null(sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); + sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue = 1; + + sponsor.Details = new SponsorDetails { Days = 11, Space = 51m }; + + context.ChangeTracker.DetectChanges(); + + var detailsEntry = sponsorEntry.Reference(s => s.Details).TargetEntry; + detailsEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue = 1; + + await context.SaveChangesAsync(); + + var newSponsorVersion = sponsorEntry.Property("Version").CurrentValue; + var newDetailsVersion = detailsEntry.Property("Version").CurrentValue; + + Assert.Equal(newSponsorVersion, newDetailsVersion); + Assert.NotEqual(sponsorVersion, newSponsorVersion); + + Assert.Equal(1, sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); + Assert.Equal(1, detailsEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); + }); + } + + public override void Property_entry_original_value_is_set() + { + base.Property_entry_original_value_is_set(); + + AssertSql( + """ +SELECT e."Id", e."EngineSupplierId", e."Name", e."StorageLocation_Latitude", e."StorageLocation_Longitude" +FROM "Engines" AS e +ORDER BY e."Id" NULLS FIRST +LIMIT 1 +""", + // + """ +@p1='1' +@p2='Mercedes' +@p0='FO 108X' +@p3='ChangedEngine' +@p4='47.64491' (Nullable = true) +@p5='-122.128101' (Nullable = true) + +UPDATE "Engines" SET "Name" = @p0 +WHERE "Id" = @p1 AND "EngineSupplierId" = @p2 AND "Name" = @p3 AND "StorageLocation_Latitude" = @p4 AND "StorageLocation_Longitude" = @p5; +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/OverzealousInitializationGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/OverzealousInitializationGaussDBTest.cs new file mode 100644 index 0000000000..864ffec94c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/OverzealousInitializationGaussDBTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore; + +public class OverzealousInitializationGaussDBTest(OverzealousInitializationGaussDBTest.OverzealousInitializationGaussDBFixture fixture) + : OverzealousInitializationTestBase(fixture) +{ + public class OverzealousInitializationGaussDBFixture : OverzealousInitializationFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Properties/AssemblyInfo.cs b/test/EFCore.GaussDB.FunctionalTests/Properties/AssemblyInfo.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/Properties/AssemblyInfo.cs rename to test/EFCore.GaussDB.FunctionalTests/Properties/AssemblyInfo.cs diff --git a/test/EFCore.GaussDB.FunctionalTests/PropertyValuesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/PropertyValuesGaussDBTest.cs new file mode 100644 index 0000000000..0585cf5e73 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/PropertyValuesGaussDBTest.cs @@ -0,0 +1,28 @@ +namespace Microsoft.EntityFrameworkCore; + +public class PropertyValuesGaussDBTest(PropertyValuesGaussDBTest.PropertyValuesGaussDBFixture fixture) + : PropertyValuesTestBase(fixture) +{ + public class PropertyValuesGaussDBFixture : PropertyValuesFixtureBase + { + protected override string StoreName { get; } = "PropertyValues"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(e => e.TerminationDate).HasColumnType("timestamp without time zone"); + + modelBuilder.Entity() + .Property(b => b.Value).HasColumnType("decimal(18,2)"); + + modelBuilder.Entity() + .Property(ce => ce.LeaveBalance).HasColumnType("decimal(18,2)"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocAdvancedMappingsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocAdvancedMappingsQueryGaussDBTest.cs new file mode 100644 index 0000000000..5f570e3401 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocAdvancedMappingsQueryGaussDBTest.cs @@ -0,0 +1,38 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocAdvancedMappingsQueryGaussDBTest(NonSharedFixture fixture) + : AdHocAdvancedMappingsQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + // Cannot write DateTime with Kind=Unspecified to GaussDB 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 GaussDB 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 GaussDB 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 GaussDB 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 GaussDB 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 GaussDB 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.GaussDB.FunctionalTests/Query/AdHocComplexTypeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocComplexTypeQueryGaussDBTest.cs new file mode 100644 index 0000000000..81677d7aeb --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocComplexTypeQueryGaussDBTest.cs @@ -0,0 +1,30 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocComplexTypeQueryGaussDBTest(NonSharedFixture fixture) : AdHocComplexTypeQueryTestBase(fixture) +{ + 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_Id='1' (Nullable = true) +@entity_equality_container_Containee1_Id='2' (Nullable = true) +@entity_equality_container_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_Id AND e."ComplexContainer_Containee1_Id" = @entity_equality_container_Containee1_Id AND e."ComplexContainer_Containee2_Id" = @entity_equality_container_Containee2_Id +LIMIT 2 +"""); + } + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocJsonQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocJsonQueryGaussDBTest.cs new file mode 100644 index 0000000000..253ff173e7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocJsonQueryGaussDBTest.cs @@ -0,0 +1,317 @@ +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class AdHocJsonQueryGaussDBTest(NonSharedFixture fixture) : AdHocJsonQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override async Task Seed29219(DbContext ctx) + { + var entity1 = new Context29219.MyEntity + { + Id = 1, + Reference = new Context29219.MyJsonEntity { NonNullableScalar = 10, NullableScalar = 11 }, + Collection = + [ + new() { NonNullableScalar = 100, NullableScalar = 101 }, + new() { NonNullableScalar = 200, NullableScalar = 201 }, + new() { NonNullableScalar = 300, NullableScalar = null } + ] + }; + + var entity2 = new Context29219.MyEntity + { + Id = 2, + Reference = new Context29219.MyJsonEntity { 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 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) +"""); + } + + #region BadJsonProperties + + // GaussDB stores JSON as jsonb, which doesn't allow badly-formed JSON; so the following tests are irrelevant. + + public override async Task Bad_json_properties_duplicated_navigations(bool noTracking) + { + if (noTracking) + { + await Assert.ThrowsAsync(() => base.Bad_json_properties_duplicated_navigations(noTracking: true)); + } + else + { + await base.Bad_json_properties_duplicated_navigations(noTracking: false); + } + } + + public override Task Bad_json_properties_duplicated_scalars(bool noTracking) + => Assert.ThrowsAsync(() => base.Bad_json_properties_duplicated_scalars(noTracking)); + + public override Task Bad_json_properties_empty_navigations(bool noTracking) + => Assert.ThrowsAsync(() => base.Bad_json_properties_empty_navigations(noTracking)); + + public override Task Bad_json_properties_empty_scalars(bool noTracking) + => Assert.ThrowsAsync(() => base.Bad_json_properties_empty_scalars(noTracking)); + + public override Task Bad_json_properties_null_navigations(bool noTracking) + => Assert.ThrowsAsync(() => base.Bad_json_properties_null_navigations(noTracking)); + + public override Task Bad_json_properties_null_scalars(bool noTracking) + => Assert.ThrowsAsync(() => base.Bad_json_properties_null_scalars(noTracking)); + + protected override Task SeedBadJsonProperties(ContextBadJsonProperties ctx) + => throw new NotSupportedException("GaussDB stores JSON as jsonb, which doesn't allow badly-formed JSON"); + + #endregion + + [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; } + } + + #region Problematc tests (Unspecified DateTime) + + // These tests use a model with a non-UTC DateTime, which isn't supported in PG's timestamp with time zone + + public override Task Project_entity_with_json_null_values() + => Assert.ThrowsAsync(() => base.Project_entity_with_json_null_values()); + + public override Task Try_project_collection_but_JSON_is_entity() + => Assert.ThrowsAsync(() => base.Try_project_collection_but_JSON_is_entity()); + + public override Task Try_project_reference_but_JSON_is_collection() + => Assert.ThrowsAsync(() => base.Try_project_reference_but_JSON_is_collection()); + + public override Task Project_entity_with_optional_json_entity_owned_by_required_json() + => Assert.ThrowsAsync(() => base.Project_entity_with_optional_json_entity_owned_by_required_json()); + + public override Task Project_required_json_entity() + => Assert.ThrowsAsync(() => base.Project_required_json_entity()); + + public override Task Project_optional_json_entity_owned_by_required_json_entity() + => Assert.ThrowsAsync(() => base.Project_optional_json_entity_owned_by_required_json_entity()); + + public override Task Project_missing_required_scalar(bool async) + => Assert.ThrowsAsync(() => base.Project_missing_required_scalar(async)); + + public override Task Project_nested_json_entity_with_missing_scalars(bool async) + => Assert.ThrowsAsync(() => base.Project_nested_json_entity_with_missing_scalars(async)); + + public override Task Project_null_required_scalar(bool async) + => Assert.ThrowsAsync(() => base.Project_null_required_scalar(async)); + + public override Task Project_root_entity_with_missing_required_navigation(bool async) + => Assert.ThrowsAsync(() => base.Project_root_entity_with_missing_required_navigation(async)); + + public override Task Project_root_entity_with_null_required_navigation(bool async) + => Assert.ThrowsAsync(() => base.Project_root_entity_with_null_required_navigation(async)); + + public override Task Project_root_with_missing_scalars(bool async) + => Assert.ThrowsAsync(() => base.Project_root_with_missing_scalars(async)); + + public override Task Project_top_level_json_entity_with_missing_scalars(bool async) + => Assert.ThrowsAsync(() => base.Project_top_level_json_entity_with_missing_scalars(async)); + + public override Task Project_missing_required_navigation(bool async) + => Task.CompletedTask; // Different exception expected in the base implementation + + public override Task Project_null_required_navigation(bool async) + => Task.CompletedTask; // Different exception expected in the base implementation + + public override Task Project_top_level_entity_with_null_value_required_scalars(bool async) + => Task.CompletedTask; // Different exception expected in the base implementation + + #endregion Problematc tests (Unspecified DateTime) + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocManyToManyQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocManyToManyQueryGaussDBTest.cs new file mode 100644 index 0000000000..9c46b0db59 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocManyToManyQueryGaussDBTest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocManyToManyQueryGaussDBTest(NonSharedFixture fixture) : AdHocManyToManyQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocMiscellaneousQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocMiscellaneousQueryGaussDBTest.cs new file mode 100644 index 0000000000..0dc24bbc69 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocMiscellaneousQueryGaussDBTest.cs @@ -0,0 +1,39 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocMiscellaneousQueryGaussDBTest(NonSharedFixture fixture) : AdHocMiscellaneousQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode) + { + new GaussDBDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); + + return optionsBuilder; + } + + // Unlike the other providers, EFCore.GaussDB does actually support mapping JsonElement + public override Task Mapping_JsonElement_property_throws_a_meaningful_exception() + => Task.CompletedTask; + + 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; + + // Writes DateTime with Kind=Unspecified to timestamptz + public override Task Subquery_first_member_compared_to_null(bool async) + => Task.CompletedTask; + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/27995/files#r874038747")] + public override Task StoreType_for_UDF_used(bool async) + => base.StoreType_for_UDF_used(async); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocNavigationsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocNavigationsQueryGaussDBTest.cs new file mode 100644 index 0000000000..89fe3c7eb2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocNavigationsQueryGaussDBTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocNavigationsQueryGaussDBTest(NonSharedFixture fixture) : AdHocNavigationsQueryRelationalTestBase(fixture) +{ + // Cannot write DateTime with Kind=Local to GaussDB 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 + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocPrecompiledQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocPrecompiledQueryGaussDBTest.cs new file mode 100644 index 0000000000..aa0b4ea66b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocPrecompiledQueryGaussDBTest.cs @@ -0,0 +1,119 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocPrecompiledQueryGaussDBTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) + : AdHocPrecompiledQueryRelationalTestBase(fixture, 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='1' + +SELECT j."Id", j."IntList", j."JsonThing" +FROM "JsonEntities" AS j +WHERE j."IntList"[@id + 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 + => GaussDBTestStoreFactory.Instance; + + protected override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers + => GaussDBPrecompiledQueryTestHelpers.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 GaussDBDbContextOptionsBuilder(builder); + sqlServerOptionsBuilder.ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocQueryFiltersQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocQueryFiltersQueryGaussDBTest.cs new file mode 100644 index 0000000000..dfcec7d383 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocQueryFiltersQueryGaussDBTest.cs @@ -0,0 +1,8 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class AdHocQueryFiltersQueryGaussDBTest(NonSharedFixture fixture) + : AdHocQueryFiltersQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/AdHocQuerySplittingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocQuerySplittingQueryGaussDBTest.cs new file mode 100644 index 0000000000..178534ab75 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/AdHocQuerySplittingQueryGaussDBTest.cs @@ -0,0 +1,48 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class AdHocQuerySplittingQueryGaussDBTest(NonSharedFixture fixture) : AdHocQuerySplittingQueryTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + private static readonly FieldInfo _querySplittingBehaviorFieldInfo = + typeof(RelationalOptionsExtension).GetField("_querySplittingBehavior", BindingFlags.NonPublic | BindingFlags.Instance); + + protected override DbContextOptionsBuilder SetQuerySplittingBehavior( + DbContextOptionsBuilder optionsBuilder, + QuerySplittingBehavior splittingBehavior) + { + new GaussDBDbContextOptionsBuilder(optionsBuilder).UseQuerySplittingBehavior(splittingBehavior); + + return optionsBuilder; + } + + protected override DbContextOptionsBuilder ClearQuerySplittingBehavior(DbContextOptionsBuilder optionsBuilder) + { + var extension = optionsBuilder.Options.FindExtension(); + if (extension == null) + { + extension = new GaussDBOptionsExtension(); + } + else + { + _querySplittingBehaviorFieldInfo.SetValue(extension, null); + } + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + + protected override TestStore CreateTestStore25225() + { + var testStore = GaussDBTestStore.Create(StoreName); + testStore.UseConnectionString = true; + return testStore; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ArrayArrayQueryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayArrayQueryTest.cs new file mode 100644 index 0000000000..df38d721e1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayArrayQueryTest.cs @@ -0,0 +1,905 @@ +using Microsoft.EntityFrameworkCore.TestModels.Array; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ArrayArrayQueryTest(ArrayArrayQueryTest.ArrayArrayQueryFixture fixture, ITestOutputHelper testOutputHelper) + : ArrayQueryTest(fixture, testOutputHelper) +{ + #region Indexers + + public override async Task Index_with_constant(bool async) + { + await base.Index_with_constant(async); + + 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 s."IntArray"[1] = 3 +"""); + } + + public override async Task Index_with_parameter(bool async) + { + await base.Index_with_parameter(async); + + AssertSql( + """ +@x='0' + +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 + 1] = 3 +"""); + } + + public override async Task Nullable_index_with_constant(bool async) + { + await base.Nullable_index_with_constant(async); + + 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 s."NullableIntArray"[1] = 3 +"""); + } + + public override async Task Nullable_value_array_index_compare_to_null(bool async) + { + await base.Nullable_value_array_index_compare_to_null(async); + + 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 s."NullableIntArray"[3] IS NULL +"""); + } + + public override async Task Non_nullable_value_array_index_compare_to_null(bool async) + { + await base.Non_nullable_value_array_index_compare_to_null(async); + + 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 FALSE +"""); + } + + public override async Task Nullable_reference_array_index_compare_to_null(bool async) + { + await base.Nullable_reference_array_index_compare_to_null(async); + + 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 s."NullableStringArray"[3] IS NULL +"""); + } + + public override async Task Non_nullable_reference_array_index_compare_to_null(bool async) + { + await base.Non_nullable_reference_array_index_compare_to_null(async); + + 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 FALSE +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Index_bytea_with_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.Bytea[0] == 3)); + + 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 get_byte(s."Bytea", 0) = 3 +"""); + } + + #endregion + + #region SequenceEqual + + public override async Task SequenceEqual_with_parameter(bool async) + { + await base.SequenceEqual_with_parameter(async); + + AssertSql( + """ +@arr={ '3', '4' } (DbType = Object) + +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 +"""); + } + + public override async Task SequenceEqual_with_array_literal(bool async) + { + await base.SequenceEqual_with_array_literal(async); + + AssertSql( + """ +SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" +FROM "SomeEntities" AS s +WHERE s."IntArray" = ARRAY[3,4]::integer[] +"""); + } + + public override async Task SequenceEqual_over_nullable_with_parameter(bool async) + { + await base.SequenceEqual_over_nullable_with_parameter(async); + + AssertSql( + """ +@arr={ '3', '4', NULL } (DbType = Object) + +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 +"""); + } + + #endregion SequenceEqual + + #region Containment + + public override async Task Array_column_Any_equality_operator(bool async) + { + await base.Array_column_Any_equality_operator(async); + + 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 s."StringArray" @> ARRAY['3']::text[] +"""); + } + + public override async Task Array_column_Any_Equals(bool async) + { + await base.Array_column_Any_Equals(async); + + 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 s."StringArray" @> ARRAY['3']::text[] +"""); + } + + public override async Task Array_column_Contains_literal_item(bool async) + { + await base.Array_column_Contains_literal_item(async); + + 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 s."IntArray" @> ARRAY[3]::integer[] +"""); + } + + public override async Task Array_column_Contains_parameter_item(bool async) + { + await base.Array_column_Contains_parameter_item(async); + + AssertSql( + """ +@p='3' + +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]::integer[] +"""); + } + + public override async Task Array_column_Contains_column_item(bool async) + { + await base.Array_column_Contains_column_item(async); + + 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 s."IntArray" @> ARRAY[s."Id" + 2]::integer[] +"""); + } + + public override async Task Array_column_Contains_null_constant(bool async) + { + await base.Array_column_Contains_null_constant(async); + + 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_position(s."NullableStringArray", NULL) IS NOT NULL +"""); + } + + public override void Array_column_Contains_null_parameter_does_not_work() + { + using var ctx = CreateContext(); + + string? p = null; + + // We incorrectly miss arrays containing non-constant nulls, because detecting those + // would prevent index use. + Assert.Equal( + 0, + ctx.SomeEntities.Count(e => e.StringArray.Contains(p))); + + AssertSql( + """ +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE s."StringArray" @> ARRAY[NULL]::text[] +"""); + } + + public override async Task Nullable_array_column_Contains_literal_item(bool async) + { + await base.Nullable_array_column_Contains_literal_item(async); + + 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 s."NullableIntArray" @> ARRAY[3]::integer[] +"""); + } + + public override async Task Array_constant_Contains_column(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText))); + + 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 s."NullableText" IN ('foo', 'xxx') +"""); + } + + public override async Task Array_param_Contains_nullable_column(bool async) + { + var array = new[] { "foo", "xxx" }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableText))); + + AssertSql( + """ +@array={ 'foo', 'xxx' } (DbType = Object) + +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) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override async Task Array_param_Contains_non_nullable_column(bool async) + { + var array = new[] { 1 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.Id))); + + AssertSql( + """ +@array={ '1' } (DbType = Object) + +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) +"""); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE s."NonNullableText" = ANY (@array) +"""); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE NOT (s."NonNullableText" = ANY (@array) AND s."NonNullableText" = ANY (@array) IS NOT NULL) +"""); + } + + public override void Array_param_with_null_Contains_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE s."NullableText" = ANY (@array) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override void Array_param_with_null_Contains_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE NOT (s."NullableText" = ANY (@array) AND s."NullableText" = ANY (@array) IS NOT NULL) AND (s."NullableText" IS NOT NULL OR array_position(@array, NULL) IS NULL) +"""); + } + + public override async Task Array_param_Contains_column_with_ToString(bool async) + { + var values = new[] { "1", "999" }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.Id.ToString()))); + + AssertSql( + """ +@values={ '1', '999' } (DbType = Object) + +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) +"""); + } + + public override async Task Byte_array_parameter_contains_column(bool async) + { + var values = new byte[] { 20 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.Byte))); + + // Note: EF Core prints the parameter as a bytea, but it's actually a smallint[] (otherwise ANY would fail) + AssertSql( + """ +@values='0x14' (DbType = Object) + +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) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_enum_to_int(bool async) + { + var array = new[] { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToInt))); + + AssertSql( + """ +@array={ '-2', '-3' } (DbType = Object) + +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) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_enum_to_string(bool async) + { + var array = new[] { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToString))); + + AssertSql( + """ +@array={ 'Two', 'Three' } (DbType = Object) + +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) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_nullable_enum_to_string(bool async) + { + var array = new SomeEnum?[] { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToString))); + + AssertSql( + """ +@array={ 'Two', 'Three' } (DbType = Object) + +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) OR (s."NullableEnumConvertedToString" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_nullable_enum_to_string_with_non_nullable_lambda(bool async) + { + var array = new SomeEnum?[] { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToStringWithNonNullableLambda))); + + AssertSql( + """ +@array={ 'Two', 'Three' } (DbType = Object) + +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) OR (s."NullableEnumConvertedToStringWithNonNullableLambda" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override async Task Array_column_Contains_value_converted_param(bool async) + { + var item = SomeEnum.Eight; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Contains(item))); + + AssertSql( + """ +@item='Eight' (Nullable = false) + +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."ValueConvertedArrayOfEnum" @> ARRAY[@item]::text[] +"""); + } + + public override async Task Array_column_Contains_value_converted_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Contains(SomeEnum.Eight))); + + 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 s."ValueConvertedArrayOfEnum" @> ARRAY['Eight']::text[] +"""); + } + + public override async Task Array_param_Contains_value_converted_array_column(bool async) + { + var p = new[] { SomeEnum.Eight, SomeEnum.Nine }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.All(x => p.Contains(x)))); + + AssertSql( + """ +@p={ 'Eight', 'Nine' } (DbType = Object) + +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."ValueConvertedArrayOfEnum" <@ @p +"""); + } + + public override async Task Array_column_Contains_in_scalar_subquery(bool async) + { + await base.Array_column_Contains_in_scalar_subquery(async); + + AssertSql( + """ +SELECT s."Id" +FROM "SomeEntityContainers" AS s +WHERE 3 = ANY (( + SELECT s0."NullableIntArray" + FROM "SomeEntities" AS s0 + WHERE s."Id" = s0."ArrayContainerEntityId" + ORDER BY s0."Id" NULLS FIRST + LIMIT 1)::integer[]) +"""); + } + + public override async Task IList_column_contains_constant(bool async) + { + await base.IList_column_contains_constant(async); + + 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 s."IList" @> ARRAY[10]::integer[] +"""); + } + + #endregion Containment + + #region Length/Count + + public override async Task Array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Length == 2)); + + 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 cardinality(s."IntArray") = 2 +"""); + } + + public override async Task Nullable_array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray.Length == 3)); + + 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 cardinality(s."NullableIntArray") = 3 +"""); + } + + public override async Task Array_Length_on_EF_Property(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => EF.Property(e, nameof(ArrayEntity.IntArray)).Length == 2)); + + 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 cardinality(s."IntArray") = 2 +"""); + } + + #endregion Length/Count + + #region Any/All + + public override async Task Any_no_predicate(bool async) + { + await base.Any_no_predicate(async); + + 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 cardinality(s."IntArray") > 0 +"""); + } + + public override async Task Any_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); + + 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 s."NullableText" LIKE ANY (ARRAY['a%','b%','c%']::text[]) +"""); + } + + public override async Task Any_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText!, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); + + 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 s."NullableText" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) +"""); + } + + public override async Task Any_like_anonymous(bool async) + { + await using var ctx = CreateContext(); + + var patternsActual = new[] { "a%", "b%", "c%" }; + var patternsExpected = new[] { "a", "b", "c" }; + + await AssertQuery( + async, + ss => ss.Set() + .Where(e => patternsActual.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => patternsExpected.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); + + AssertSql( + """ +@patternsActual={ 'a%', 'b%', 'c%' } (DbType = Object) + +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) +"""); + } + + public override async Task All_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "b%", "ba%" }.All(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "b", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); + + 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 s."NullableText" LIKE ALL (ARRAY['b%','ba%']::text[]) +"""); + } + + public override async Task All_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "B%", "ba%" }.All(p => EF.Functions.ILike(e.NullableText!, p))), + ss => ss.Set() + .Where(e => new[] { "B", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); + + 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 s."NullableText" ILIKE ALL (ARRAY['B%','ba%']::text[]) +"""); + } + + public override async Task Any_Contains_on_constant_array(bool async) + { + await base.Any_Contains_on_constant_array(async); + + 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[2,3]::integer[] && s."IntArray" +"""); + } + + public override async Task Any_Contains_between_column_and_List(bool async) + { + await base.Any_Contains_between_column_and_List(async); + + AssertSql( + """ +@ints={ '2', '3' } (DbType = Object) + +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 +"""); + } + + public override async Task Any_Contains_between_column_and_array(bool async) + { + await base.Any_Contains_between_column_and_array(async); + + AssertSql( + """ +@ints={ '2', '3' } (DbType = Object) + +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 +"""); + } + + public override async Task Any_Contains_between_column_and_other_type(bool async) + { + var list = new List { SomeEnum.Eight }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Any(i => list.Contains(i)))); + + AssertSql( + """ +@list={ 'Eight' } (DbType = Object) + +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."ValueConvertedArrayOfEnum" && @list +"""); + } + + public override async Task All_Contains(bool async) + { + await base.All_Contains(async); + + 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[5,6]::integer[] <@ s."IntArray" +"""); + } + + #endregion Any/All + + #region Other translations + + public override async Task Append(bool async) + // TODO: https://github.com/dotnet/efcore/issues/30669 + => await AssertTranslationFailed(() => base.Append(async)); + + // await base.Append(async); + // + // AssertSql( + // """ + // SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" + // FROM "SomeEntities" AS s + // WHERE array_append(s."IntArray", 5) = ARRAY[3,4,5]::integer[] + // """); + public override async Task Concat(bool async) + { + await base.Concat(async); + + AssertSql( + """ +SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" +FROM "SomeEntities" AS s +WHERE array_cat(s."IntArray", ARRAY[5,6]::integer[]) = ARRAY[3,4,5,6]::integer[] +"""); + } + + public override async Task Array_IndexOf1(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => Array.IndexOf(e.IntArray, 6) == 1)); + + 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 COALESCE(array_position(s."IntArray", 6) - 1, -1) = 1 +"""); + } + + public override async Task Array_IndexOf2(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => Array.IndexOf(e.IntArray, 6, 1) == 1)); + + 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 COALESCE(array_position(s."IntArray", 6, 2) - 1, -1) = 1 +"""); + } + + // Note: see NorthwindFunctionsQueryGaussDBTest.String_Join_non_aggregate for regular use without an array column/parameter + public override async Task String_Join_with_array_of_int_column(bool async) + { + await base.String_Join_with_array_of_int_column(async); + + 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."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 + { + protected override string StoreName + => "ArrayQueryTest"; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ArrayListQueryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayListQueryTest.cs new file mode 100644 index 0000000000..84f0f9d4af --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayListQueryTest.cs @@ -0,0 +1,1009 @@ +using Microsoft.EntityFrameworkCore.TestModels.Array; +using TypeExtensions = HuaweiCloud.EntityFrameworkCore.GaussDB.TypeExtensions; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ArrayListQueryTest : ArrayQueryTest +{ + public ArrayListQueryTest(ArrayListQueryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) + { + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Indexers + + public override async Task Index_with_constant(bool async) + { + await base.Index_with_constant(async); + + 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 s."IntList"[1] = 3 +"""); + } + + public override async Task Index_with_parameter(bool async) + { + await base.Index_with_parameter(async); + + AssertSql( + """ +@x='0' + +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 + 1] = 3 +"""); + } + + public override async Task Nullable_index_with_constant(bool async) + { + await base.Nullable_index_with_constant(async); + + 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 s."NullableIntList"[1] = 3 +"""); + } + + public override async Task Nullable_value_array_index_compare_to_null(bool async) + { + await base.Nullable_value_array_index_compare_to_null(async); + + 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 s."NullableIntList"[3] IS NULL +"""); + } + + public override async Task Non_nullable_value_array_index_compare_to_null(bool async) + { + await base.Non_nullable_value_array_index_compare_to_null(async); + + 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 FALSE +"""); + } + + public override async Task Nullable_reference_array_index_compare_to_null(bool async) + { + await base.Nullable_reference_array_index_compare_to_null(async); + + 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 s."NullableStringList"[3] IS NULL +"""); + } + + public override async Task Non_nullable_reference_array_index_compare_to_null(bool async) + { + await base.Non_nullable_reference_array_index_compare_to_null(async); + + 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 FALSE +"""); + } + + #endregion + + #region SequenceEqual + + public override async Task SequenceEqual_with_parameter(bool async) + { + await base.SequenceEqual_with_parameter(async); + + AssertSql( + """ +@arr={ '3', '4' } (DbType = Object) + +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 +"""); + } + + public override async Task SequenceEqual_with_array_literal(bool async) + { + await base.SequenceEqual_with_array_literal(async); + + AssertSql( + """ +SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" +FROM "SomeEntities" AS s +WHERE s."IntList" = ARRAY[3,4]::integer[] +"""); + } + + public override async Task SequenceEqual_over_nullable_with_parameter(bool async) + { + await base.SequenceEqual_over_nullable_with_parameter(async); + + AssertSql( + """ +@arr={ '3', '4', NULL } (DbType = Object) + +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 +"""); + } + + #endregion SequenceEqual + + #region Containment + + public override async Task Array_column_Any_equality_operator(bool async) + { + await base.Array_column_Any_equality_operator(async); + + 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 s."StringList" @> ARRAY['3']::text[] +"""); + } + + public override async Task Array_column_Any_Equals(bool async) + { + await base.Array_column_Any_Equals(async); + + 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 s."StringList" @> ARRAY['3']::text[] +"""); + } + + public override async Task Array_column_Contains_literal_item(bool async) + { + await base.Array_column_Contains_literal_item(async); + + 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 s."IntList" @> ARRAY[3]::integer[] +"""); + } + + public override async Task Array_column_Contains_parameter_item(bool async) + { + await base.Array_column_Contains_parameter_item(async); + + AssertSql( + """ +@p='3' + +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]::integer[] +"""); + } + + public override async Task Array_column_Contains_column_item(bool async) + { + await base.Array_column_Contains_column_item(async); + + 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 s."IntList" @> ARRAY[s."Id" + 2]::integer[] +"""); + } + + public override async Task Array_column_Contains_null_constant(bool async) + { + await base.Array_column_Contains_null_constant(async); + + 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_position(s."NullableStringList", NULL) IS NOT NULL +"""); + } + + public override void Array_column_Contains_null_parameter_does_not_work() + { + using var ctx = CreateContext(); + + string? p = null; + + // We incorrectly miss arrays containing non-constant nulls, because detecting those + // would prevent index use. + Assert.Equal( + 0, + ctx.SomeEntities.Count(e => e.StringList.Contains(p!))); + + AssertSql( + """ +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE s."StringList" @> ARRAY[NULL]::text[] +"""); + } + + public override async Task Nullable_array_column_Contains_literal_item(bool async) + { + await base.Nullable_array_column_Contains_literal_item(async); + + 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 s."NullableIntList" @> ARRAY[3]::integer[] +"""); + } + + public override async Task Array_constant_Contains_column(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText))); + + 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 s."NullableText" IN ('foo', 'xxx') +"""); + } + + public override async Task Array_param_Contains_nullable_column(bool async) + { + var array = new List { "foo", "xxx" }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableText!))); + + AssertSql( + """ +@array={ 'foo', 'xxx' } (DbType = Object) + +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) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override async Task Array_param_Contains_non_nullable_column(bool async) + { + var array = new List { 1 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.Id))); + + AssertSql( + """ +@array={ '1' } (DbType = Object) + +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) +"""); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE s."NonNullableText" = ANY (@array) +"""); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE NOT (s."NonNullableText" = ANY (@array) AND s."NonNullableText" = ANY (@array) IS NOT NULL) +"""); + } + + public override void Array_param_with_null_Contains_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NullableText))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE s."NullableText" = ANY (@array) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override void Array_param_with_null_Contains_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NullableText!))); + + AssertSql( + """ +@array={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT count(*)::int +FROM "SomeEntities" AS s +WHERE NOT (s."NullableText" = ANY (@array) AND s."NullableText" = ANY (@array) IS NOT NULL) AND (s."NullableText" IS NOT NULL OR array_position(@array, NULL) IS NULL) +"""); + } + + public override async Task Array_param_Contains_column_with_ToString(bool async) + { + var values = new List { "1", "999" }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.Id.ToString()))); + + AssertSql( + """ +@values={ '1', '999' } (DbType = Object) + +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) +"""); + } + + public override async Task Byte_array_parameter_contains_column(bool async) + { + var values = new List { 20 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.Byte))); + + AssertSql( + """ +@values={ '20' } (DbType = Object) + +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) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_enum_to_int(bool async) + { + var array = new List { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToInt))); + + AssertSql( + """ +@array={ '-2', '-3' } (DbType = Object) + +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) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_enum_to_string(bool async) + { + var array = new List { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToString))); + + AssertSql( + """ +@array={ 'Two', 'Three' } (DbType = Object) + +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) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_nullable_enum_to_string(bool async) + { + var array = new List { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToString))); + + AssertSql( + """ +@array={ 'Two', 'Three' } (DbType = Object) + +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) OR (s."NullableEnumConvertedToString" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override async Task Array_param_Contains_value_converted_column_nullable_enum_to_string_with_non_nullable_lambda(bool async) + { + var array = new List { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToStringWithNonNullableLambda))); + + AssertSql( + """ +@array={ 'Two', 'Three' } (DbType = Object) + +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) OR (s."NullableEnumConvertedToStringWithNonNullableLambda" IS NULL AND array_position(@array, NULL) IS NOT NULL) +"""); + } + + public override async Task Array_column_Contains_value_converted_param(bool async) + { + var item = SomeEnum.Eight; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Contains(item))); + + AssertSql( + """ +@item='Eight' (Nullable = false) + +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."ValueConvertedListOfEnum" @> ARRAY[@item]::text[] +"""); + } + + public override async Task Array_column_Contains_value_converted_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Contains(SomeEnum.Eight))); + + 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 s."ValueConvertedListOfEnum" @> ARRAY['Eight']::text[] +"""); + } + + public override async Task Array_param_Contains_value_converted_array_column(bool async) + { + var p = new List { SomeEnum.Eight, SomeEnum.Nine }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.All(x => p.Contains(x)))); + + AssertSql( + """ +@p={ 'Eight', 'Nine' } (DbType = Object) + +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."ValueConvertedListOfEnum" <@ @p +"""); + } + + public override async Task IList_column_contains_constant(bool async) + { + await base.IList_column_contains_constant(async); + + 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 s."IList" @> ARRAY[10]::integer[] +"""); + } + + public override async Task Array_column_Contains_in_scalar_subquery(bool async) + { + await base.Array_column_Contains_in_scalar_subquery(async); + + AssertSql( + """ +SELECT s."Id" +FROM "SomeEntityContainers" AS s +WHERE 3 = ANY (( + SELECT s0."NullableIntList" + FROM "SomeEntities" AS s0 + WHERE s."Id" = s0."ArrayContainerEntityId" + ORDER BY s0."Id" NULLS FIRST + LIMIT 1)::integer[]) +"""); + } + + #endregion Containment + + #region Length/Count + + public override async Task Array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntList.Count == 2)); + + 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 cardinality(s."IntList") = 2 +"""); + } + + public override async Task Nullable_array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntList.Count == 3)); + + 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 cardinality(s."NullableIntList") = 3 +"""); + } + + public override async Task Array_Length_on_EF_Property(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => EF.Property>(e, nameof(ArrayEntity.IntList)).Count == 2)); + + 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 cardinality(s."IntList") = 2 +"""); + } + + #endregion Length/Count + + #region Any/All + + public override async Task Any_no_predicate(bool async) + { + await base.Any_no_predicate(async); + + 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 cardinality(s."IntList") > 0 +"""); + } + + public override async Task Any_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); + + 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 s."NullableText" LIKE ANY (ARRAY['a%','b%','c%']::text[]) +"""); + } + + public override async Task Any_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText!, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); + + 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 s."NullableText" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) +"""); + } + + public override async Task Any_like_anonymous(bool async) + { + await using var ctx = CreateContext(); + + var patternsActual = new List + { + "a%", + "b%", + "c%" + }; + var patternsExpected = new List + { + "a", + "b", + "c" + }; + + await AssertQuery( + async, + ss => ss.Set() + .Where(e => patternsActual.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => patternsExpected.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); + + AssertSql( + """ +@patternsActual={ 'a%', 'b%', 'c%' } (DbType = Object) + +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) +"""); + } + + public override async Task All_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new List { "b%", "ba%" }.All(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new List { "b", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); + + 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 s."NullableText" LIKE ALL (ARRAY['b%','ba%']::text[]) +"""); + } + + public override async Task All_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new List { "B%", "ba%" }.All(p => EF.Functions.ILike(e.NullableText!, p))), + ss => ss.Set() + .Where(e => new List { "B", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); + + 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 s."NullableText" ILIKE ALL (ARRAY['B%','ba%']::text[]) +"""); + } + + public override async Task Any_Contains_on_constant_array(bool async) + { + await base.Any_Contains_on_constant_array(async); + + 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[2,3]::integer[] && s."IntList" +"""); + } + + public override async Task Any_Contains_between_column_and_List(bool async) + { + await base.Any_Contains_between_column_and_List(async); + + AssertSql( + """ +@ints={ '2', '3' } (DbType = Object) + +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 +"""); + } + + public override async Task Any_Contains_between_column_and_array(bool async) + { + await base.Any_Contains_between_column_and_array(async); + + AssertSql( + """ +@ints={ '2', '3' } (DbType = Object) + +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 +"""); + } + + public override async Task Any_Contains_between_column_and_other_type(bool async) + { + var array = new[] { SomeEnum.Eight }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Any(i => array.Contains(i)))); + + AssertSql( + """ +@array={ 'Eight' } (DbType = Object) + +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."ValueConvertedListOfEnum" && @array +"""); + } + + public override async Task All_Contains(bool async) + { + await base.All_Contains(async); + + 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[5,6]::integer[] <@ s."IntList" +"""); + } + + #endregion Any/All + + #region Other translations + + public override async Task Append(bool async) + // TODO: https://github.com/dotnet/efcore/issues/30669 + => await AssertTranslationFailed(() => base.Append(async)); + + // await base.Append(async); + // + // AssertSql( + // """ + // SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" + // FROM "SomeEntities" AS s + // WHERE array_append(s."IntList", 5) = ARRAY[3,4,5]::integer[] + // """); + public override async Task Concat(bool async) + { + await base.Concat(async); + + AssertSql( + """ +SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" +FROM "SomeEntities" AS s +WHERE array_cat(s."IntList", ARRAY[5,6]::integer[]) = ARRAY[3,4,5,6]::integer[] +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task Array_IndexOf1(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntList.IndexOf(6) == 1)); + + 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 COALESCE(array_position(s."IntList", 6) - 1, -1) = 1 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task Array_IndexOf2(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntList.IndexOf(6, 1) == 1)); + + 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 COALESCE(array_position(s."IntList", 6, 2) - 1, -1) = 1 +"""); + } + + // Note: see NorthwindFunctionsQueryGaussDBTest.String_Join_non_aggregate for regular use without an array column/parameter + public override async Task String_Join_with_array_of_int_column(bool async) + { + await base.String_Join_with_array_of_int_column(async); + + 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."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 + { + protected override string StoreName + => "ArrayListTest"; + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + => new ArrayToListReplacingExpressionVisitor().Visit(serverQueryExpression); + + private class ArrayToListReplacingExpressionVisitor : ExpressionVisitor + { + private static readonly PropertyInfo IntArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.IntArray))!; + + private static readonly PropertyInfo NullableIntArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableIntArray))!; + + private static readonly PropertyInfo IntList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.IntList))!; + + private static readonly PropertyInfo NullableIntList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableIntList))!; + + private static readonly PropertyInfo StringArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.StringArray))!; + + private static readonly PropertyInfo NullableStringArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableStringArray))!; + + private static readonly PropertyInfo StringList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.StringList))!; + + private static readonly PropertyInfo NullableStringList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableStringList))!; + + private static readonly PropertyInfo ValueConvertedArrayOfEnum + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedArrayOfEnum))!; + + private static readonly PropertyInfo ValueConvertedListOfEnum + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedListOfEnum))!; + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Member == IntArray) + { + return Expression.MakeMemberAccess(node.Expression, IntList); + } + + if (node.Member == NullableIntArray) + { + return Expression.MakeMemberAccess(node.Expression, NullableIntList); + } + + if (node.Member == StringArray) + { + return Expression.MakeMemberAccess(node.Expression, StringList); + } + + if (node.Member == NullableStringArray) + { + return Expression.MakeMemberAccess(node.Expression, NullableStringList); + } + + if (node.Member == ValueConvertedArrayOfEnum) + { + return Expression.MakeMemberAccess(node.Expression, ValueConvertedListOfEnum); + } + + return node; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ArrayIndex) + { + var listExpression = Visit(node.Left); + if (TypeExtensions.IsGenericList(listExpression.Type)) + { + var getItemMethod = listExpression.Type.GetMethod("get_Item", [typeof(int)])!; + return Expression.Call(listExpression, getItemMethod, node.Right); + } + } + + return base.VisitBinary(node); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayQueryFixture.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/ArrayQueryFixture.cs index 7ea9d7176d..7544b8a3e3 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayQueryFixture.cs @@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Query; public abstract class ArrayQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory { protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayQueryTest.cs new file mode 100644 index 0000000000..c9ab3484fd --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ArrayQueryTest.cs @@ -0,0 +1,531 @@ +using Microsoft.EntityFrameworkCore.TestModels.Array; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; + +// ReSharper disable ConvertToConstant.Local + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class ArrayQueryTest : QueryTestBase + where TFixture : ArrayQueryFixture, new() +{ + // ReSharper disable once UnusedParameter.Local + public ArrayQueryTest(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Roundtrip + + [ConditionalFact] + public void Roundtrip() + { + using var ctx = CreateContext(); + var x = ctx.SomeEntities.Single(e => e.Id == 1); + + Assert.Equal(new[] { 3, 4 }, x.IntArray); + Assert.Equal([3, 4], x.IntList); + Assert.Equal([3, 4, null], x.NullableIntArray); + Assert.Equal( + [ + 3, + 4, + null + ], x.NullableIntList); + } + + #endregion + + #region Indexers + + [Theory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Index_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray[0] == 3)); + + [Theory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Index_with_parameter(bool async) + { + // ReSharper disable once ConvertToConstant.Local + var x = 0; + + return AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray[x] == 3)); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Nullable_index_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray[0] == 3)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Nullable_value_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.NullableIntArray[2] == null)); + +#pragma warning disable CS0472 + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Non_nullable_value_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray[1] == null), + assertEmpty: true); +#pragma warning restore CS0472 + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Nullable_reference_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.NullableStringArray[2] == null)); + +#pragma warning disable CS0472 + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Non_nullable_reference_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(e => e.StringArray[1] == null), + assertEmpty: true); +#pragma warning restore CS0472 + + #endregion Indexers + + #region SequenceEqual + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SequenceEqual_with_parameter(bool async) + { + var arr = new[] { 3, 4 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.SequenceEqual(arr))); + } + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/30786")] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SequenceEqual_with_array_literal(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.SequenceEqual(new[] { 3, 4 }))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SequenceEqual_over_nullable_with_parameter(bool async) + { + var arr = new int?[] { 3, 4, null }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray.SequenceEqual(arr))); + } + + #endregion + + #region Containment + + // See also tests in NorthwindMiscellaneousQueryGaussDBTest + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Any_equality_operator(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.StringArray.Any(p => p == "3"))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Any_Equals(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.StringArray.Any(p => "3".Equals(p)))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_literal_item(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Contains(3))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_parameter_item(bool async) + { + var p = 3; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Contains(p))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_column_item(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Contains(e.Id + 2))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_null_constant(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableStringArray.Contains(null))); + + [ConditionalFact] + public abstract void Array_column_Contains_null_parameter_does_not_work(); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Nullable_array_column_Contains_literal_item(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray.Contains(3))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_constant_Contains_column(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_nullable_column(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_non_nullable_column(bool async); + + [ConditionalFact] + public abstract void Array_param_with_null_Contains_non_nullable_not_found(); + + [ConditionalFact] + public abstract void Array_param_with_null_Contains_non_nullable_not_found_negated(); + + [ConditionalFact] + public abstract void Array_param_with_null_Contains_nullable_not_found(); + + [ConditionalFact] + public abstract void Array_param_with_null_Contains_nullable_not_found_negated(); + + [ConditionalTheory] // #2123 + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_column_with_ToString(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Byte_array_parameter_contains_column(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_column_enum_to_int(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_column_enum_to_string(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_column_nullable_enum_to_string(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_column_nullable_enum_to_string_with_non_nullable_lambda(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_column_Contains_value_converted_param(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_column_Contains_value_converted_constant(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_array_column(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_in_scalar_subquery(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(c => c.ArrayEntities.OrderBy(e => e.Id).First().NullableIntArray.Contains(3))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task IList_column_contains_constant(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(a => a.IList.Contains(10))); + + #endregion + + #region Length/Count + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_Length(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Nullable_array_Length(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_Length_on_EF_Property(bool async); + + #endregion Length/Count + + #region Any/All + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_no_predicate(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Any())); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_like(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_ilike(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_like_anonymous(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task All_like(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task All_ilike(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_Contains_on_constant_array(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { 2, 3 }.Any(p => e.IntArray.Contains(p)))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_Contains_between_column_and_List(bool async) + { + var ints = new List { 2, 3 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Any(i => ints.Contains(i)))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_Contains_between_column_and_array(bool async) + { + var ints = new[] { 2, 3 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Any(i => ints.Contains(i)))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_Contains_between_column_and_other_type(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task All_Contains(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { 5, 6 }.All(p => e.IntArray.Contains(p)))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_like_column(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.StringArray.Any(s => EF.Functions.Like(s, "3"))), + ss => ss.Set().Where(e => e.StringArray.Any(s => s.Contains("3")))); + + #endregion Any/All + + #region New + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task New_array_with_columns(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(e => new[] { e.NullableText, e.NonNullableText }), + elementAsserter: Assert.Equal, + elementSorter: strings => strings != null ? string.Join(separator: "", strings) : ""); + + AssertSql( + """ +SELECT ARRAY[s."NullableText",s."NonNullableText"]::text[] +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task New_array_with_heterogeneous_columns_throws(bool async) + { + // Note that arrays of objects are treated specially by EF Core, so they're fine. + // The below checks Bytea and ByteArray, which are the same CLR type (byte[]) but mapped to different PG types + // (bytea and smallint[]) + await using var context = CreateContext(); + + var exception = async + ? await Assert.ThrowsAsync( + () => context.Set().Select(e => new[] { e.Bytea, e.ByteArray }).ToListAsync()) + : Assert.Throws( + () => context.Set().Select(e => new[] { e.Bytea, e.ByteArray }).ToList()); + + Assert.Equal(GaussDBStrings.HeterogeneousTypesInNewArray("bytea", "smallint[]"), exception.Message); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task New_array_with_heterogeneous_columns_but_same_base_type(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(e => new[] { e.Varchar10, e.Varchar15 }), + elementAsserter: Assert.Equal, + elementSorter: strings => strings != null ? string.Join(separator: "", strings) : ""); + + AssertSql( + """ +SELECT ARRAY[s."Varchar10",s."Varchar15"]::varchar(15)[] +FROM "SomeEntities" AS s +"""); + } + + [Theory] // #2342 + [MemberData(nameof(IsAsyncData))] + public async Task New_array_with_heterogeneous_columns_but_textual(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(e => new[] { e.NonNullableText, e.Varchar15 }), + elementAsserter: Assert.Equal, + elementSorter: strings => strings != null ? string.Join(separator: "", strings) : ""); + + AssertSql( + """ +SELECT ARRAY[s."NonNullableText",s."Varchar15"]::text[] +FROM "SomeEntities" AS s +"""); + } + + [Theory] // #2342 + [MemberData(nameof(IsAsyncData))] + public async Task New_array_with_heterogeneous_columns_but_textual_after_ToString(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(e => new[] { e.Id.ToString(), e.Varchar15 }), + elementAsserter: Assert.Equal, + elementSorter: strings => strings != null ? string.Join(separator: "", strings) : ""); + + AssertSql( + """ +SELECT ARRAY[s."Id"::text,s."Varchar15"]::text[] +FROM "SomeEntities" AS s +"""); + } + + [Theory] // #2688 + [MemberData(nameof(IsAsyncData))] + public async Task New_array_VisitChildren(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(e => new[] { e.NonNullableText, e.NullableText ?? "" }), + elementAsserter: Assert.Equal, + elementSorter: strings => strings != null ? string.Join(separator: "", strings) : ""); + + AssertSql( + """ +SELECT ARRAY[s."NonNullableText",COALESCE(s."NullableText", '')]::text[] +FROM "SomeEntities" AS s +"""); + } + + #endregion + + #region Other translations + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Append(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.IntArray.Append(5).SequenceEqual(new[] { 3, 4, 5 }))); + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/30786")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Concat(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.IntArray.Concat(new[] { 5, 6 }).SequenceEqual(new[] { 3, 4, 5, 6 }))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_IndexOf1(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_IndexOf2(bool async); + + // Note: see NorthwindFunctionsQueryGaussDBTest.String_Join_non_aggregate for regular use without an array column/parameter + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + 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 + + protected ArrayQueryContext CreateContext() + => Fixture.CreateContext(); + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + #endregion +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/CharacterQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/CharacterQueryGaussDBTest.cs new file mode 100644 index 0000000000..8afac534d2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/CharacterQueryGaussDBTest.cs @@ -0,0 +1,225 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class CharacterQueryGaussDBTest : IClassFixture +{ + private CharacterQueryGaussDBFixture Fixture { get; } + + // ReSharper disable once UnusedParameter.Local + public CharacterQueryGaussDBTest(CharacterQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [Fact] + public void Find_in_database() + { + Fixture.ClearEntities(); + + // important: add here so they aren't locally available below. + using (var ctx = CreateContext()) + { + ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "12345678" }); + ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "123456 " }); + ctx.SaveChanges(); + } + + using (var ctx = CreateContext()) + { + const string update = "update"; + + var m1 = ctx.CharacterTestEntities.Find("12345678"); + Assert.NotNull(m1); + m1.Character6 = update; + ctx.SaveChanges(); + + var m2 = ctx.CharacterTestEntities.Find("123456 "); + Assert.NotNull(m2); + m2.Character6 = update; + ctx.SaveChanges(); + + var item0 = ctx.CharacterTestEntities.Find("12345678")!.Character6; + Assert.Equal(update, item0); + + var item1 = ctx.CharacterTestEntities.Find("123456 ")!.Character6; + Assert.Equal(update, item1); + } + } + + [Fact] + public void Find_locally_available() + { + Fixture.ClearEntities(); + + // important: add here so they are locally available below. + using var ctx = CreateContext(); + ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "12345678" }); + ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "123456 " }); + ctx.SaveChanges(); + + const string update = "update"; + + var m1 = ctx.CharacterTestEntities.Find("12345678")!; + m1.Character6 = update; + ctx.SaveChanges(); + + var m2 = ctx.CharacterTestEntities.Find("123456 ")!; + m2.Character6 = update; + ctx.SaveChanges(); + + var item0 = ctx.CharacterTestEntities.Find("12345678")!.Character6; + Assert.Equal(update, item0); + + var item1 = ctx.CharacterTestEntities.Find("123456 ")!.Character6; + Assert.Equal(update, item1); + } + + /// + /// Test something like: select '123456 '::char(8) = '123456'::char(8); + /// + [Fact] + public void Test_change_tracking() + { + Fixture.ClearEntities(); + + using var ctx = CreateContext(); + const string update = "update"; + + ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "12345678" }); + ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "123456 " }); + ctx.SaveChanges(); + + var m1 = ctx.CharacterTestEntities.Find("12345678")!; + m1.Character6 = update; + ctx.SaveChanges(); + + var m2 = ctx.CharacterTestEntities.Find("123456 ")!; + m2.Character6 = update; + ctx.SaveChanges(); + } + + /// + /// Test that comparisons are treated correctly. + /// + [Fact] + public void Test_change_tracking_key_sizes() + { + Fixture.ClearEntities(); + + using (var ctx = CreateContext()) + { + var entity = new CharacterTestEntity { Character8 = "123456 ", Character6 = "12345 " }; + ctx.CharacterTestEntities.Add(entity); + ctx.SaveChanges(); + + // In memory, the properties are unchanged. + Assert.Equal("12345 ", entity.Character6); + + // Trailing whitespace is ignored when querying. + var fromLocal = ctx.CharacterTestEntities.Single(x => x.Character6 == "12345"); + + // And since we queried the same context, we received the same object. + Assert.Equal(entity, fromLocal); + + // Which means that the property actually still has trailing whitespace... + Assert.Equal("12345 ", fromLocal.Character6); + + // No changes are detected/saved when trailing whitespace is added. + entity.Character6 += " "; + Assert.Equal(0, ctx.SaveChanges()); + } + + using (var ctx = CreateContext()) + { + // The query still ignores the trailing whitespace, + // but the materialized object won't have any trailing whitespace. + var fromDb = ctx.CharacterTestEntities.Single(x => x.Character6 == "12345 "); + + // BUG: Why isn't the local cache clean? This shouldn't have the trailing whitespace. + Assert.Equal("12345 ", fromDb.Character6); + } + } + + #region Fixture + + // ReSharper disable once ClassNeverInstantiated.Global + /// + /// Represents a fixture suitable for testing character data. + /// + public class CharacterQueryGaussDBFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "CharacterQueryGaussDBTest"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + /// + /// Clears the entities in the context. + /// + public void ClearEntities() + { + using var ctx = CreateContext(); + var entities = ctx.CharacterTestEntities.ToArray(); + foreach (var e in entities) + { + ctx.CharacterTestEntities.Remove(e); + } + + ctx.SaveChanges(); + } + } + + public class CharacterTestEntity + { + public string? Character8 { get; set; } + public string? Character6 { get; set; } + } + + public class CharacterContext : PoolableDbContext + { + public DbSet CharacterTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public CharacterContext(DbContextOptions options) + : base(options) + { + } + + /// + protected override void OnModelCreating(ModelBuilder builder) + => builder.Entity( + entity => + { + entity.HasKey(e => e.Character8); + entity.Property(e => e.Character8).HasColumnType("character(8)"); + entity.Property(e => e.Character6).HasColumnType("character(6)"); + }); + } + + #endregion + + #region Helpers + + protected CharacterContext CreateContext() + => Fixture.CreateContext(); + + // ReSharper disable once UnusedMember.Global + /// + /// 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, Fixture.TestSqlLoggerFactory.Sql); + + #endregion +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/CompatibilityQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/CompatibilityQueryGaussDBTest.cs new file mode 100644 index 0000000000..1d531245b9 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/CompatibilityQueryGaussDBTest.cs @@ -0,0 +1,116 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class CompatibilityQueryGaussDBTest : IClassFixture +{ + private CompatibilityQueryGaussDBFixture Fixture { get; } + + // ReSharper disable once UnusedParameter.Local + public CompatibilityQueryGaussDBTest(CompatibilityQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public async Task Array_contains_is_not_parameterized_with_array_on_redshift() + { + var ctx = CreateRedshiftContext(); + + // https://github.com/dotnet/efcore/issues/36311 + await Assert.ThrowsAsync(async () => + { + var numbers = new[] { 8, 9 }; + var result = await ctx.TestEntities.Where(e => numbers.Contains(e.SomeInt)).SingleAsync(); + Assert.Equal(1, result.Id); + + AssertSql( + """ + SELECT t."Id", t."SomeInt" + FROM "TestEntities" AS t + WHERE t."SomeInt" IN (?, ?) + LIMIT 2 + """); + }); + } + + #region Support + + private CompatibilityContext CreateContext(Version? postgresVersion = null) + => Fixture.CreateContext(postgresVersion); + + private CompatibilityContext CreateRedshiftContext() + => Fixture.CreateRedshiftContext(); + + public class CompatibilityQueryGaussDBFixture : FixtureBase, IDisposable, IAsyncLifetime + { + private TestStore _testStore = null!; + + private const string StoreName = "CompatibilityTest"; + private readonly ListLoggerFactory _listLoggerFactory = GaussDBTestStoreFactory.Instance.CreateListLoggerFactory(_ => false); + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)_listLoggerFactory; + + public virtual CompatibilityContext CreateContext() + => CreateContext(null); + + public virtual CompatibilityContext CreateContext(Version? postgresVersion) + { + var builder = new DbContextOptionsBuilder(); + _testStore.AddProviderOptions(builder); + builder + .UseGaussDB(o => o.SetPostgresVersion(postgresVersion)) + .UseLoggerFactory(_listLoggerFactory); + return new CompatibilityContext(builder.Options); + } + + public virtual CompatibilityContext CreateRedshiftContext() + { + var builder = new DbContextOptionsBuilder(); + _testStore.AddProviderOptions(builder); + builder + .UseGaussDB(o => o.UseRedshift()) + .UseLoggerFactory(_listLoggerFactory); + return new CompatibilityContext(builder.Options); + } + + public virtual async Task InitializeAsync() + { + _testStore = GaussDBTestStoreFactory.Instance.GetOrCreate(StoreName); + await _testStore.InitializeAsync(null, CreateContext, c => CompatibilityContext.SeedAsync((CompatibilityContext)c)); + } + + // Called after DisposeAsync + public virtual void Dispose() + { + } + + public virtual async Task DisposeAsync() + => await _testStore.DisposeAsync(); + } + + public class CompatibilityTestEntity + { + public int Id { get; set; } + public int SomeInt { get; set; } + } + + public class CompatibilityContext(DbContextOptions options) : DbContext(options) + { + public DbSet TestEntities { get; set; } + + public static async Task SeedAsync(CompatibilityContext context) + { + context.TestEntities.AddRange( + new CompatibilityTestEntity { Id = 1, SomeInt = 8 }, + new CompatibilityTestEntity { Id = 2, SomeInt = 10 }); + await context.SaveChangesAsync(); + } + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + #endregion Support +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsQueryGaussDBTest.cs new file mode 100644 index 0000000000..54b28d18f0 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsQueryGaussDBTest.cs @@ -0,0 +1,14 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsCollectionsQueryGaussDBTest : ComplexNavigationsCollectionsQueryRelationalTestBase< + ComplexNavigationsQueryGaussDBFixture> +{ + public ComplexNavigationsCollectionsQueryGaussDBTest( + ComplexNavigationsQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQueryGaussDBTest.cs new file mode 100644 index 0000000000..085c524690 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQueryGaussDBTest.cs @@ -0,0 +1,14 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsCollectionsSharedTypeQueryGaussDBTest : ComplexNavigationsCollectionsSharedTypeQueryRelationalTestBase< + ComplexNavigationsSharedTypeQueryGaussDBFixture> +{ + public ComplexNavigationsCollectionsSharedTypeQueryGaussDBTest( + ComplexNavigationsSharedTypeQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQueryGaussDBTest.cs new file mode 100644 index 0000000000..87a673016a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQueryGaussDBTest.cs @@ -0,0 +1,14 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsCollectionsSplitQueryGaussDBTest : ComplexNavigationsCollectionsSplitQueryRelationalTestBase< + ComplexNavigationsQueryGaussDBFixture> +{ + public ComplexNavigationsCollectionsSplitQueryGaussDBTest( + ComplexNavigationsQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryGaussDBTest.cs new file mode 100644 index 0000000000..0d5a084463 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryGaussDBTest.cs @@ -0,0 +1,15 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsCollectionsSplitSharedTypeQueryGaussDBTest : + ComplexNavigationsCollectionsSplitSharedTypeQueryRelationalTestBase< + ComplexNavigationsSharedTypeQueryGaussDBFixture> +{ + public ComplexNavigationsCollectionsSplitSharedTypeQueryGaussDBTest( + ComplexNavigationsSharedTypeQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsQueryGaussDBFixture.cs new file mode 100644 index 0000000000..ccda90b04f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsQueryGaussDBFixture.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsQueryGaussDBFixture : ComplexNavigationsQueryRelationalFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(l => l.Date).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(l => l.Date).HasColumnType("timestamp without time zone"); + } + // public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + // { + // var optionsBuilder = base.AddOptions(builder); + // new GaussDBDbContextOptionsBuilder(optionsBuilder).ReverseNullOrdering(); + // return optionsBuilder; + // } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsQueryGaussDBTest.cs new file mode 100644 index 0000000000..0a7c85e9fe --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsQueryGaussDBTest.cs @@ -0,0 +1,23 @@ +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsQueryGaussDBTest : ComplexNavigationsQueryRelationalTestBase +{ + public ComplexNavigationsQueryGaussDBTest(ComplexNavigationsQueryGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + 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)); + + public override Task GroupJoin_client_method_in_OrderBy(bool async) + => AssertTranslationFailedWithDetails( + () => base.GroupJoin_client_method_in_OrderBy(async), + CoreStrings.QueryUnableToTranslateMethod( + "Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase", + "ClientMethodNullableInt")); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryGaussDBFixture.cs new file mode 100644 index 0000000000..35f4428485 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryGaussDBFixture.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsSharedTypeQueryGaussDBFixture : ComplexNavigationsSharedTypeQueryRelationalFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(l => l.Date).HasColumnType("timestamp without time zone"); + modelBuilder.Entity("Level1.OneToOne_Required_PK1#Level2").Property("Date").HasColumnType("timestamp without time zone"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryGaussDBTest.cs new file mode 100644 index 0000000000..500d8e6ffa --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryGaussDBTest.cs @@ -0,0 +1,40 @@ +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexNavigationsSharedTypeQueryGaussDBTest + : ComplexNavigationsSharedTypeQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public ComplexNavigationsSharedTypeQueryGaussDBTest( + ComplexNavigationsSharedTypeQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [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)); + + public override Task GroupJoin_client_method_in_OrderBy(bool async) + => AssertTranslationFailedWithDetails( + () => base.GroupJoin_client_method_in_OrderBy(async), + CoreStrings.QueryUnableToTranslateMethod( + "Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase", + "ClientMethodNullableInt")); + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/26104")] + public override Task GroupBy_aggregate_where_required_relationship(bool async) + => base.GroupBy_aggregate_where_required_relationship(async); + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/26104")] + public override Task GroupBy_aggregate_where_required_relationship_2(bool async) + => base.GroupBy_aggregate_where_required_relationship_2(async); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ComplexTypeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexTypeQueryGaussDBTest.cs new file mode 100644 index 0000000000..d2f58584ff --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ComplexTypeQueryGaussDBTest.cs @@ -0,0 +1,1167 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class ComplexTypeQueryGaussDBTest : ComplexTypeQueryRelationalTestBase< + ComplexTypeQueryGaussDBTest.ComplexTypeQueryGaussDBFixture> +{ + public ComplexTypeQueryGaussDBTest(ComplexTypeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Filter_on_property_inside_complex_type(bool async) + { + await base.Filter_on_property_inside_complex_type(async); + + AssertSql( + """ +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 +"""); + } + + public override async Task Filter_on_property_inside_nested_complex_type(bool async) + { + await base.Filter_on_property_inside_nested_complex_type(async); + + AssertSql( + """ +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' +"""); + } + + public override async Task Filter_on_property_inside_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_complex_type_after_subquery(async); + + AssertSql( + """ +@p='1' + +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 + OFFSET @p +) AS c0 +WHERE c0."ShippingAddress_ZipCode" = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_nested_complex_type_after_subquery(async); + + AssertSql( + """ +@p='1' + +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 + OFFSET @p +) AS c0 +WHERE c0."ShippingAddress_Country_Code" = 'DE' +"""); + } + + public override async Task Filter_on_required_property_inside_required_complex_type_on_optional_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_complex_type_on_optional_navigation(async); + + AssertSql( + """ +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" +WHERE c0."ShippingAddress_ZipCode" <> 7728 OR c0."ShippingAddress_ZipCode" IS NULL +"""); + } + + public override async Task Filter_on_required_property_inside_required_complex_type_on_required_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_complex_type_on_required_navigation(async); + + AssertSql( + """ +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" +WHERE c0."ShippingAddress_ZipCode" <> 7728 +"""); + } + + // This test fails because when OptionalCustomer is null, we get all-null results because of the LEFT JOIN, and we materialize this + // as an empty ShippingAddress instead of null (see SQL). The proper solution here would be to project the Customer ID just for the + // purpose of knowing that it's there. + public override async Task Project_complex_type_via_optional_navigation(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Project_complex_type_via_optional_navigation(async)); + + Assert.Equal(RelationalStrings.CannotProjectNullableComplexType("Customer.ShippingAddress#Address"), exception.Message); + } + + public override async Task Project_complex_type_via_required_navigation(bool async) + { + await base.Project_complex_type_via_required_navigation(async); + + AssertSql( + """ +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" +"""); + } + + public override async Task Load_complex_type_after_subquery_on_entity_type(bool async) + { + await base.Load_complex_type_after_subquery_on_entity_type(async); + + AssertSql( + """ +@p='1' + +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 + OFFSET @p +) AS c0 +"""); + } + + public override async Task Select_complex_type(bool async) + { + await base.Select_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" +FROM "Customer" AS c +"""); + } + + public override async Task Select_nested_complex_type(bool async) + { + await base.Select_nested_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" +FROM "Customer" AS c +"""); + } + + public override async Task Select_single_property_on_nested_complex_type(bool async) + { + await base.Select_single_property_on_nested_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_Country_FullName" +FROM "Customer" AS c +"""); + } + + public override async Task Select_complex_type_Where(bool async) + { + await base.Select_complex_type_Where(async); + + AssertSql( + """ +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 +"""); + } + + public override async Task Select_complex_type_Distinct(bool async) + { + await base.Select_complex_type_Distinct(async); + + AssertSql( + """ +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 +"""); + } + + public override async Task Complex_type_equals_complex_type(bool async) + { + await base.Complex_type_equals_complex_type(async); + + AssertSql( + """ +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_Tags" = c."BillingAddress_Tags" AND c."ShippingAddress_ZipCode" = c."BillingAddress_ZipCode" +"""); + } + + public override async Task Complex_type_equals_constant(bool async) + { + await base.Complex_type_equals_constant(async); + + AssertSql( + """ +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_Tags" = ARRAY['foo','bar']::text[] AND c."ShippingAddress_ZipCode" = 38654 AND c."ShippingAddress_Country_Code" = 'US' AND c."ShippingAddress_Country_FullName" = 'United States' +"""); + } + + public override async Task Complex_type_equals_parameter(bool async) + { + await base.Complex_type_equals_parameter(async); + + AssertSql( + """ +@entity_equality_address_AddressLine1='804 S. Lakeshore Road' +@entity_equality_address_Tags={ 'foo', 'bar' } (DbType = Object) +@entity_equality_address_ZipCode='38654' (Nullable = true) +@entity_equality_address_Country_Code='US' +@entity_equality_address_Country_FullName='United States' + +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_AddressLine1 AND c."ShippingAddress_AddressLine2" IS NULL AND c."ShippingAddress_Tags" = @entity_equality_address_Tags AND c."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND c."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND c."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName +"""); + } + + public override async Task Complex_type_equals_null(bool async) + { + await base.Complex_type_equals_null(async); + + AssertSql(); + } + + public override async Task Subquery_over_complex_type(bool async) + { + await base.Subquery_over_complex_type(async); + + AssertSql(); + } + + public override async Task Contains_over_complex_type(bool async) + { + await base.Contains_over_complex_type(async); + + AssertSql( + """ +@entity_equality_address_AddressLine1='804 S. Lakeshore Road' +@entity_equality_address_Tags={ 'foo', 'bar' } (DbType = Object) +@entity_equality_address_ZipCode='38654' (Nullable = true) +@entity_equality_address_Country_Code='US' +@entity_equality_address_Country_FullName='United States' + +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_AddressLine1 AND c0."ShippingAddress_AddressLine2" IS NULL AND c0."ShippingAddress_Tags" = @entity_equality_address_Tags AND c0."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND c0."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND c0."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName) +"""); + } + + public override async Task Concat_complex_type(bool async) + { + await base.Concat_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" +FROM "Customer" AS c +WHERE c."Id" = 1 +UNION ALL +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 +"""); + } + + public override async Task Concat_entity_type_containing_complex_property(bool async) + { + await base.Concat_entity_type_containing_complex_property(async); + + AssertSql( + """ +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_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 +"""); + } + + public override async Task Union_entity_type_containing_complex_property(bool async) + { + await base.Union_entity_type_containing_complex_property(async); + + AssertSql( + """ +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_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 +"""); + } + + public override async Task Union_complex_type(bool async) + { + await base.Union_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" +FROM "Customer" AS c +WHERE c."Id" = 1 +UNION +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 +"""); + } + + public override async Task Concat_property_in_complex_type(bool async) + { + await base.Concat_property_in_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_AddressLine1" +FROM "Customer" AS c +UNION ALL +SELECT c0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" +FROM "Customer" AS c0 +"""); + } + + public override async Task Union_property_in_complex_type(bool async) + { + await base.Union_property_in_complex_type(async); + + AssertSql( + """ +SELECT c."ShippingAddress_AddressLine1" +FROM "Customer" AS c +UNION +SELECT c0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" +FROM "Customer" AS c0 +"""); + } + + public override async Task Concat_two_different_complex_type(bool async) + { + await base.Concat_two_different_complex_type(async); + + AssertSql(); + } + + public override async Task Union_two_different_complex_type(bool async) + { + await base.Union_two_different_complex_type(async); + + AssertSql(); + } + + public override async Task Filter_on_property_inside_struct_complex_type(bool async) + { + await base.Filter_on_property_inside_struct_complex_type(async); + + AssertSql( + """ +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_ZipCode" = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_struct_complex_type(bool async) + { + await base.Filter_on_property_inside_nested_struct_complex_type(async); + + AssertSql( + """ +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_Country_Code" = 'DE' +"""); + } + + public override async Task Filter_on_property_inside_struct_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_struct_complex_type_after_subquery(async); + + AssertSql( + """ +@p='1' + +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 +) AS v0 +WHERE v0."ShippingAddress_ZipCode" = 7728 +"""); + } + + public override async Task Filter_on_property_inside_nested_struct_complex_type_after_subquery(bool async) + { + await base.Filter_on_property_inside_nested_struct_complex_type_after_subquery(async); + + AssertSql( + """ +@p='1' + +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 +) AS v0 +WHERE v0."ShippingAddress_Country_Code" = 'DE' +"""); + } + + public override async Task Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(async); + + AssertSql( + """ +SELECT v."Id", v."OptionalCustomerId", v."RequiredCustomerId", 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", v1."Id", v1."Name", v1."BillingAddress_AddressLine1", v1."BillingAddress_AddressLine2", v1."BillingAddress_ZipCode", v1."BillingAddress_Country_Code", v1."BillingAddress_Country_FullName", v1."ShippingAddress_AddressLine1", v1."ShippingAddress_AddressLine2", v1."ShippingAddress_ZipCode", v1."ShippingAddress_Country_Code", v1."ShippingAddress_Country_FullName" +FROM "ValuedCustomerGroup" AS v +LEFT JOIN "ValuedCustomer" AS v0 ON v."OptionalCustomerId" = v0."Id" +INNER JOIN "ValuedCustomer" AS v1 ON v."RequiredCustomerId" = v1."Id" +WHERE v0."ShippingAddress_ZipCode" <> 7728 OR v0."ShippingAddress_ZipCode" IS NULL +"""); + } + + public override async Task Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(bool async) + { + await base.Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(async); + + AssertSql( + """ +SELECT v."Id", v."OptionalCustomerId", v."RequiredCustomerId", v1."Id", v1."Name", v1."BillingAddress_AddressLine1", v1."BillingAddress_AddressLine2", v1."BillingAddress_ZipCode", v1."BillingAddress_Country_Code", v1."BillingAddress_Country_FullName", v1."ShippingAddress_AddressLine1", v1."ShippingAddress_AddressLine2", v1."ShippingAddress_ZipCode", v1."ShippingAddress_Country_Code", v1."ShippingAddress_Country_FullName", 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 "ValuedCustomerGroup" AS v +INNER JOIN "ValuedCustomer" AS v0 ON v."RequiredCustomerId" = v0."Id" +LEFT JOIN "ValuedCustomer" AS v1 ON v."OptionalCustomerId" = v1."Id" +WHERE v0."ShippingAddress_ZipCode" <> 7728 +"""); + } + + // This test fails because when OptionalCustomer is null, we get all-null results because of the LEFT JOIN, and we materialize this + // as an empty ShippingAddress instead of null (see SQL). The proper solution here would be to project the Customer ID just for the + // purpose of knowing that it's there. + public override async Task Project_struct_complex_type_via_optional_navigation(bool async) + { + var exception = + await Assert.ThrowsAsync(() => base.Project_struct_complex_type_via_optional_navigation(async)); + + Assert.Equal(RelationalStrings.CannotProjectNullableComplexType("ValuedCustomer.ShippingAddress#AddressStruct"), exception.Message); + } + + public override async Task Project_struct_complex_type_via_required_navigation(bool async) + { + await base.Project_struct_complex_type_via_required_navigation(async); + + AssertSql( + """ +SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" +FROM "ValuedCustomerGroup" AS v +INNER JOIN "ValuedCustomer" AS v0 ON v."RequiredCustomerId" = v0."Id" +"""); + } + + public override async Task Load_struct_complex_type_after_subquery_on_entity_type(bool async) + { + await base.Load_struct_complex_type_after_subquery_on_entity_type(async); + + AssertSql( + """ +@p='1' + +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 +) AS v0 +"""); + } + + public override async Task Select_struct_complex_type(bool async) + { + await base.Select_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +"""); + } + + public override async Task Select_nested_struct_complex_type(bool async) + { + await base.Select_nested_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +"""); + } + + public override async Task Select_single_property_on_nested_struct_complex_type(bool async) + { + await base.Select_single_property_on_nested_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +"""); + } + + public override async Task Select_struct_complex_type_Where(bool async) + { + await base.Select_struct_complex_type_Where(async); + + AssertSql( + """ +SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +WHERE v."ShippingAddress_ZipCode" = 7728 +"""); + } + + public override async Task Select_struct_complex_type_Distinct(bool async) + { + await base.Select_struct_complex_type_Distinct(async); + + AssertSql( + """ +SELECT DISTINCT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +"""); + } + + public override async Task Struct_complex_type_equals_struct_complex_type(bool async) + { + await base.Struct_complex_type_equals_struct_complex_type(async); + + AssertSql( + """ +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" = v."BillingAddress_AddressLine1" AND (v."ShippingAddress_AddressLine2" = v."BillingAddress_AddressLine2" OR (v."ShippingAddress_AddressLine2" IS NULL AND v."BillingAddress_AddressLine2" IS NULL)) AND v."ShippingAddress_ZipCode" = v."BillingAddress_ZipCode" +"""); + } + + public override async Task Struct_complex_type_equals_constant(bool async) + { + await base.Struct_complex_type_equals_constant(async); + + AssertSql( + """ +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" = '804 S. Lakeshore Road' AND v."ShippingAddress_AddressLine2" IS NULL AND v."ShippingAddress_ZipCode" = 38654 AND v."ShippingAddress_Country_Code" = 'US' AND v."ShippingAddress_Country_FullName" = 'United States' +"""); + } + + public override async Task Struct_complex_type_equals_parameter(bool async) + { + await base.Struct_complex_type_equals_parameter(async); + + AssertSql( + """ +@entity_equality_address_AddressLine1='804 S. Lakeshore Road' +@entity_equality_address_ZipCode='38654' (Nullable = true) +@entity_equality_address_Country_Code='US' +@entity_equality_address_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_AddressLine1 AND v."ShippingAddress_AddressLine2" IS NULL AND v."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND v."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND v."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName +"""); + } + + public override async Task Subquery_over_struct_complex_type(bool async) + { + await base.Subquery_over_struct_complex_type(async); + + AssertSql(); + } + + public override async Task Contains_over_struct_complex_type(bool async) + { + await base.Contains_over_struct_complex_type(async); + + AssertSql( + """ +@entity_equality_address_AddressLine1='804 S. Lakeshore Road' +@entity_equality_address_ZipCode='38654' (Nullable = true) +@entity_equality_address_Country_Code='US' +@entity_equality_address_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_AddressLine1 AND v0."ShippingAddress_AddressLine2" IS NULL AND v0."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND v0."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND v0."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName) +"""); + } + + public override async Task Concat_struct_complex_type(bool async) + { + await base.Concat_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +WHERE v."Id" = 1 +UNION ALL +SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v0 +WHERE v0."Id" = 2 +"""); + } + + public override async Task Concat_entity_type_containing_struct_complex_property(bool async) + { + await base.Concat_entity_type_containing_struct_complex_property(async); + + AssertSql( + """ +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."Id" = 1 +UNION ALL +SELECT 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 "ValuedCustomer" AS v0 +WHERE v0."Id" = 2 +"""); + } + + public override async Task Union_entity_type_containing_struct_complex_property(bool async) + { + await base.Union_entity_type_containing_struct_complex_property(async); + + AssertSql( + """ +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."Id" = 1 +UNION +SELECT 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 "ValuedCustomer" AS v0 +WHERE v0."Id" = 2 +"""); + } + + public override async Task Union_struct_complex_type(bool async) + { + await base.Union_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v +WHERE v."Id" = 1 +UNION +SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" +FROM "ValuedCustomer" AS v0 +WHERE v0."Id" = 2 +"""); + } + + public override async Task Concat_property_in_struct_complex_type(bool async) + { + await base.Concat_property_in_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_AddressLine1" +FROM "ValuedCustomer" AS v +UNION ALL +SELECT v0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" +FROM "ValuedCustomer" AS v0 +"""); + } + + public override async Task Union_property_in_struct_complex_type(bool async) + { + await base.Union_property_in_struct_complex_type(async); + + AssertSql( + """ +SELECT v."ShippingAddress_AddressLine1" +FROM "ValuedCustomer" AS v +UNION +SELECT v0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" +FROM "ValuedCustomer" AS v0 +"""); + } + + public override async Task Concat_two_different_struct_complex_type(bool async) + { + await base.Concat_two_different_struct_complex_type(async); + + AssertSql(); + } + + public override async Task Union_two_different_struct_complex_type(bool async) + { + await base.Union_two_different_struct_complex_type(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='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 + ) 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='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 + ) 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='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 + ) 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='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 + ) 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='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 +"""); + } + + 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='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 + ) AS u0 +) AS u1 +ORDER BY u1."Id" NULLS FIRST, u1."Id0" NULLS FIRST +LIMIT @p +"""); + } + + 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='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 +"""); + } + + 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='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 + ) AS u0 +) AS u1 +ORDER BY u1."BillingAddress_ZipCode" NULLS FIRST, u1."BillingAddress_ZipCode0" NULLS FIRST +LIMIT @p +"""); + } + + 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='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 + ) 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='20' +@p0='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 + ) 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 @p0 + ) AS c2 +) AS c4 ON c3."Id" = c4."Id" +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class ComplexTypeQueryGaussDBFixture : ComplexTypeQueryRelationalFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysQueryGaussDBFixture.cs new file mode 100644 index 0000000000..0c07e96dbd --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysQueryGaussDBFixture.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.TestModels.CompositeKeysModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class CompositeKeysQueryGaussDBFixture : CompositeKeysQueryRelationalFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(c => c.Date).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(c => c.Date).HasColumnType("timestamp without time zone"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysQueryGaussDBTest.cs new file mode 100644 index 0000000000..e1c9cee425 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysQueryGaussDBTest.cs @@ -0,0 +1,13 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class CompositeKeysQueryGaussDBTest : CompositeKeysQueryRelationalTestBase +{ + public CompositeKeysQueryGaussDBTest( + CompositeKeysQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysSplitQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysSplitQueryGaussDBTest.cs new file mode 100644 index 0000000000..c5da904898 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/CompositeKeysSplitQueryGaussDBTest.cs @@ -0,0 +1,13 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class CompositeKeysSplitQueryGaussDBTest : CompositeKeysSplitQueryRelationalTestBase +{ + public CompositeKeysSplitQueryGaussDBTest( + CompositeKeysQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Ef6GroupByGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Ef6GroupByGaussDBTest.cs new file mode 100644 index 0000000000..d03b35cacf --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Ef6GroupByGaussDBTest.cs @@ -0,0 +1,95 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class Ef6GroupByGaussDBTest : Ef6GroupByTestBase +{ + public Ef6GroupByGaussDBTest(Ef6GroupByGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Whats_new_2021_sample_3(bool async) + { + await base.Whats_new_2021_sample_3(async); + + AssertSql( + """ +SELECT ( + 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 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 +"""); + } + + public override async Task Whats_new_2021_sample_5(bool async) + { + await base.Whats_new_2021_sample_5(async); + + AssertSql( + """ +SELECT ( + 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 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 +"""); + } + + public override async Task Whats_new_2021_sample_6(bool async) + { + await base.Whats_new_2021_sample_6(async); + + AssertSql( + """ +SELECT ( + 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 p0."MiddleInitial" + FROM "Person" AS p0 + WHERE p0."Age" = 20 AND p."Id" = p0."Id" + LIMIT 1) NULLS FIRST +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class Ef6GroupByGaussDBFixture : Ef6GroupByFixtureBase, ITestSqlLoggerFactory + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().Property(o => o.OrderDate).HasColumnType("timestamp"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/EntitySplittingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/EntitySplittingQueryGaussDBTest.cs new file mode 100644 index 0000000000..7237b20ba1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/EntitySplittingQueryGaussDBTest.cs @@ -0,0 +1,778 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class EntitySplittingQueryGaussDBTest(NonSharedFixture fixture) + : EntitySplittingQueryTestBase(fixture) +{ + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Can_query_entity_which_is_split_in_two(bool async) + { + await base.Can_query_entity_which_is_split_in_two(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s."StringValue3", s."StringValue4" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart" AS s ON e."Id" = s."Id" +"""); + } + + public override async Task Can_query_entity_which_is_split_selecting_only_main_properties(bool async) + { + await base.Can_query_entity_which_is_split_selecting_only_main_properties(async); + + AssertSql( + """ +SELECT e."Id", e."IntValue1", e."StringValue1" +FROM "EntityOne" AS e +"""); + } + + public override async Task Can_query_entity_which_is_split_in_three(bool async) + { + await base.Can_query_entity_which_is_split_in_three(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" +INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +"""); + } + + public override async Task Can_query_entity_which_is_split_selecting_only_part_2_properties(bool async) + { + await base.Can_query_entity_which_is_split_selecting_only_part_2_properties(async); + + AssertSql( + """ +SELECT e."Id", s."IntValue3", s."StringValue3" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart2" AS s ON e."Id" = s."Id" +"""); + } + + public override async Task Can_query_entity_which_is_split_selecting_only_part_3_properties(bool async) + { + await base.Can_query_entity_which_is_split_selecting_only_part_3_properties(async); + + AssertSql( + """ +SELECT e."Id", s."IntValue4", s."StringValue4" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" +"""); + } + + public override async Task Include_reference_to_split_entity(bool async) + { + await base.Include_reference_to_split_entity(async); + + AssertSql( + """ +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 s1 ON e."EntityOneId" = s1."Id" +"""); + } + + public override async Task Include_collection_to_split_entity(bool async) + { + await base.Include_collection_to_split_entity(async); + + AssertSql( + """ +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 s1 ON e."Id" = s1."EntityThreeId" +ORDER BY e."Id" NULLS FIRST +"""); + } + + public override async Task Include_reference_to_split_entity_including_reference(bool async) + { + await base.Include_reference_to_split_entity_including_reference(async); + + AssertSql( + """ +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 s1 ON e."EntityOneId" = s1."Id" +LEFT JOIN "EntityThree" AS e1 ON s1."EntityThreeId" = e1."Id" +"""); + } + + public override async Task Include_collection_to_split_entity_including_collection(bool async) + { + await base.Include_collection_to_split_entity_including_collection(async); + + AssertSql( + """ +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" + FROM "EntityOne" AS e0 + 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 s1 ON e."Id" = s1."EntityThreeId" +ORDER BY e."Id" NULLS FIRST, s1."Id" NULLS FIRST +"""); + } + + public override async Task Include_reference_on_split_entity(bool async) + { + await base.Include_reference_on_split_entity(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", e0."Id", e0."Name" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" +INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +LEFT JOIN "EntityThree" AS e0 ON e."EntityThreeId" = e0."Id" +"""); + } + + public override async Task Include_collection_on_split_entity(bool async) + { + await base.Include_collection_on_split_entity(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", e0."Id", e0."EntityOneId", e0."Name" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" +INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +LEFT JOIN "EntityTwo" AS e0 ON e."Id" = e0."EntityOneId" +ORDER BY e."Id" NULLS FIRST +"""); + } + + public override async Task Custom_projection_trim_when_multiple_tables(bool async) + { + await base.Custom_projection_trim_when_multiple_tables(async); + + AssertSql( + """ +SELECT e."IntValue1", s."IntValue3", e0."Id", e0."Name" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart2" AS s ON e."Id" = s."Id" +LEFT JOIN "EntityThree" AS e0 ON e."EntityThreeId" = e0."Id" +"""); + } + + public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_sharing(bool async) + { + await base.Normal_entity_owning_a_split_reference_with_main_fragment_sharing(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", e."IntValue3", e."IntValue4", e."StringValue1", e."StringValue2", e."StringValue3", e."StringValue4", e."OwnedReference_Id", e."OwnedReference_OwnedIntValue1", e."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", e."OwnedReference_OwnedStringValue1", e."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "EntityOne" AS e +LEFT JOIN "OwnedReferenceExtras2" AS o ON e."Id" = o."EntityOneId" +LEFT JOIN "OwnedReferenceExtras1" AS o0 ON e."Id" = o0."EntityOneId" +"""); + } + + public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_sharing_custom_projection(bool async) + { + await base.Normal_entity_owning_a_split_reference_with_main_fragment_sharing_custom_projection(async); + + AssertSql( + """ +SELECT e."Id", CASE + WHEN e."OwnedReference_Id" IS NOT NULL AND e."OwnedReference_OwnedIntValue1" IS NOT NULL AND e."OwnedReference_OwnedIntValue2" IS NOT NULL AND o0."OwnedIntValue3" IS NOT NULL AND o."OwnedIntValue4" IS NOT NULL THEN o."OwnedIntValue4" +END AS "OwnedIntValue4", CASE + WHEN e."OwnedReference_Id" IS NOT NULL AND e."OwnedReference_OwnedIntValue1" IS NOT NULL AND e."OwnedReference_OwnedIntValue2" IS NOT NULL AND o0."OwnedIntValue3" IS NOT NULL AND o."OwnedIntValue4" IS NOT NULL THEN o."OwnedStringValue4" +END AS "OwnedStringValue4" +FROM "EntityOnes" AS e +LEFT JOIN "OwnedReferenceExtras2" AS o ON e."Id" = o."EntityOneId" +LEFT JOIN "OwnedReferenceExtras1" AS o0 ON e."Id" = o0."EntityOneId" +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing(bool async) + { + await base.Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing_custom_projection(bool async) + { + await base.Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing_custom_projection(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Normal_entity_owning_a_split_collection(bool async) + { + await base.Normal_entity_owning_a_split_collection(async); + + AssertSql(); + } + + public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_sharing_multiple_level(bool async) + { + await base.Normal_entity_owning_a_split_reference_with_main_fragment_sharing_multiple_level(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", e."IntValue3", e."IntValue4", e."StringValue1", e."StringValue2", e."StringValue3", e."StringValue4", e."OwnedReference_Id", e."OwnedReference_OwnedIntValue1", e."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", e."OwnedReference_OwnedStringValue1", e."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4", e."OwnedReference_OwnedNestedReference_Id", e."OwnedReference_OwnedNestedReference_OwnedNestedIntValue1", e."OwnedReference_OwnedNestedReference_OwnedNestedIntValue2", o2."OwnedNestedIntValue3", o1."OwnedNestedIntValue4", e."OwnedReference_OwnedNestedReference_OwnedNestedStringValue1", e."OwnedReference_OwnedNestedReference_OwnedNestedStringValue2", o2."OwnedNestedStringValue3", o1."OwnedNestedStringValue4" +FROM "EntityOnes" AS e +LEFT JOIN "OwnedReferenceExtras2" AS o ON e."Id" = o."EntityOneId" +LEFT JOIN "OwnedReferenceExtras1" AS o0 ON e."Id" = o0."EntityOneId" +LEFT JOIN "OwnedNestedReferenceExtras2" AS o1 ON e."Id" = o1."OwnedReferenceEntityOneId" +LEFT JOIN "OwnedNestedReferenceExtras1" AS o2 ON e."Id" = o2."OwnedReferenceEntityOneId" +"""); + } + + public override async Task Split_entity_owning_a_reference(bool async) + { + await base.Split_entity_owning_a_reference(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", e."OwnedReference_Id", e."OwnedReference_OwnedIntValue1", e."OwnedReference_OwnedIntValue2", e."OwnedReference_OwnedIntValue3", e."OwnedReference_OwnedIntValue4", e."OwnedReference_OwnedStringValue1", e."OwnedReference_OwnedStringValue2", e."OwnedReference_OwnedStringValue3", e."OwnedReference_OwnedStringValue4" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" +INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +"""); + } + + public override async Task Split_entity_owning_a_collection(bool async) + { + await base.Split_entity_owning_a_collection(async); + + AssertSql( + """ +SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", o."EntityOneId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o."OwnedIntValue3", o."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o."OwnedStringValue3", o."OwnedStringValue4" +FROM "EntityOne" AS e +INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" +INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" +LEFT JOIN "OwnedCollection" AS o ON e."Id" = o."EntityOneId" +ORDER BY e."Id" NULLS FIRST, o."EntityOneId" NULLS FIRST +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Split_entity_owning_a_split_reference_without_table_sharing(bool async) + { + await base.Split_entity_owning_a_split_reference_without_table_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Split_entity_owning_a_split_collection(bool async) + { + await base.Split_entity_owning_a_split_collection(async); + + AssertSql(); + } + + public override async Task Split_entity_owning_a_split_reference_with_table_sharing_1(bool async) + { + await base.Split_entity_owning_a_split_reference_with_table_sharing_1(async); + + AssertSql( + """ +SELECT s."Id", s."EntityThreeId", s."IntValue1", s."IntValue2", s1."IntValue3", s0."IntValue4", s."StringValue1", s."StringValue2", s1."StringValue3", s0."StringValue4", s."OwnedReference_Id", s."OwnedReference_OwnedIntValue1", s."OwnedReference_OwnedIntValue2", s1."OwnedReference_OwnedIntValue3", s0."OwnedReference_OwnedIntValue4", s."OwnedReference_OwnedStringValue1", s."OwnedReference_OwnedStringValue2", s1."OwnedReference_OwnedStringValue3", s0."OwnedReference_OwnedStringValue4" +FROM "SplitEntityOnePart1" AS s +INNER JOIN "SplitEntityOnePart3" AS s0 ON s."Id" = s0."Id" +INNER JOIN "SplitEntityOnePart2" AS s1 ON s."Id" = s1."Id" +"""); + } + + public override async Task Split_entity_owning_a_split_reference_with_table_sharing_4(bool async) + { + await base.Split_entity_owning_a_split_reference_with_table_sharing_4(async); + + AssertSql( + """ +SELECT s."Id", s."EntityThreeId", s."IntValue1", s."IntValue2", s1."IntValue3", s0."IntValue4", s."StringValue1", s."StringValue2", s1."StringValue3", s0."StringValue4", s."OwnedReference_Id", s."OwnedReference_OwnedIntValue1", s."OwnedReference_OwnedIntValue2", s1."OwnedReference_OwnedIntValue3", o."OwnedIntValue4", s."OwnedReference_OwnedStringValue1", s."OwnedReference_OwnedStringValue2", s1."OwnedReference_OwnedStringValue3", o."OwnedStringValue4" +FROM "SplitEntityOnePart1" AS s +INNER JOIN "SplitEntityOnePart3" AS s0 ON s."Id" = s0."Id" +INNER JOIN "SplitEntityOnePart2" AS s1 ON s."Id" = s1."Id" +LEFT JOIN "OwnedReferencePart3" AS o ON s."Id" = o."EntityOneId" +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Split_entity_owning_a_split_reference_with_table_sharing_6(bool async) + { + await base.Split_entity_owning_a_split_reference_with_table_sharing_6(async); + + AssertSql( + """ +SELECT s."Id", s."EntityThreeId", s."IntValue1", s."IntValue2", s1."IntValue3", s0."IntValue4", s."StringValue1", s."StringValue2", s1."StringValue3", s0."StringValue4", s1."Id", s1."OwnedReference_Id", s1."OwnedReference_OwnedIntValue1", s1."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", s1."OwnedReference_OwnedStringValue1", s1."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "SplitEntityOnePart1" AS s +INNER JOIN "SplitEntityOnePart3" AS s0 ON s."Id" = s0."Id" +INNER JOIN "SplitEntityOnePart2" AS s1 ON s."Id" = s1."Id" +LEFT JOIN "OwnedReferencePart3" AS o ON s1."Id" = o."EntityOneId" +LEFT JOIN "OwnedReferencePart2" AS o0 ON s1."Id" = o0."EntityOneId" +"""); + } + + public override async Task Tph_entity_owning_a_split_reference_on_base_with_table_sharing(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_base_with_table_sharing(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", b."Discriminator", b."MiddleValue", b."SiblingValue", b."LeafValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" +"""); + } + + public override async Task Tpt_entity_owning_a_split_reference_on_base_with_table_sharing(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_base_with_table_sharing(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", m."MiddleValue", s."SiblingValue", l."LeafValue", CASE + WHEN l."Id" IS NOT NULL THEN 'LeafEntity' + WHEN s."Id" IS NOT NULL THEN 'SiblingEntity' + WHEN m."Id" IS NOT NULL THEN 'MiddleEntity' +END AS "Discriminator", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +LEFT JOIN "MiddleEntity" AS m ON b."Id" = m."Id" +LEFT JOIN "SiblingEntity" AS s ON b."Id" = s."Id" +LEFT JOIN "LeafEntity" AS l ON b."Id" = l."Id" +LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" +"""); + } + + public override async Task Tph_entity_owning_a_split_reference_on_middle_with_table_sharing(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_middle_with_table_sharing(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", b."Discriminator", b."MiddleValue", b."SiblingValue", b."LeafValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."MiddleEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."MiddleEntityId" +"""); + } + + public override async Task Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", m."MiddleValue", s."SiblingValue", l."LeafValue", CASE + WHEN l."Id" IS NOT NULL THEN 'LeafEntity' + WHEN s."Id" IS NOT NULL THEN 'SiblingEntity' + WHEN m."Id" IS NOT NULL THEN 'MiddleEntity' +END AS "Discriminator", m."Id", m."OwnedReference_Id", m."OwnedReference_OwnedIntValue1", m."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", m."OwnedReference_OwnedStringValue1", m."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +LEFT JOIN "MiddleEntity" AS m ON b."Id" = m."Id" +LEFT JOIN "SiblingEntity" AS s ON b."Id" = s."Id" +LEFT JOIN "LeafEntity" AS l ON b."Id" = l."Id" +LEFT JOIN "OwnedReferencePart4" AS o ON m."Id" = o."MiddleEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON m."Id" = o0."MiddleEntityId" +"""); + } + + public override async Task Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", b."Discriminator", b."MiddleValue", b."SiblingValue", b."LeafValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."LeafEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."LeafEntityId" +"""); + } + + public override async Task Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", m."MiddleValue", s."SiblingValue", l."LeafValue", CASE + WHEN l."Id" IS NOT NULL THEN 'LeafEntity' + WHEN s."Id" IS NOT NULL THEN 'SiblingEntity' + WHEN m."Id" IS NOT NULL THEN 'MiddleEntity' +END AS "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" +FROM "BaseEntity" AS b +LEFT JOIN "MiddleEntity" AS m ON b."Id" = m."Id" +LEFT JOIN "SiblingEntity" AS s ON b."Id" = s."Id" +LEFT JOIN "LeafEntity" AS l ON b."Id" = l."Id" +LEFT JOIN "OwnedReferencePart4" AS o ON l."Id" = o."LeafEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON l."Id" = o0."LeafEntityId" +"""); + } + + public override async Task Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing(bool async) + { + await base.Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing(async); + + AssertSql( + """ +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 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" + FROM "MiddleEntity" AS m + 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 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" +"""); + } + + public override async Task Tph_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", b."Discriminator", b."SiblingValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" +WHERE b."Discriminator" = 'SiblingEntity' +"""); + } + + public override async Task Tpt_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", s."SiblingValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" +FROM "BaseEntity" AS b +INNER JOIN "SiblingEntity" AS s ON b."Id" = s."Id" +LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" +LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" +"""); + } + + public override async Task Tph_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", b."Discriminator", b."SiblingValue" +FROM "BaseEntity" AS b +WHERE b."Discriminator" = 'SiblingEntity' +"""); + } + + public override async Task Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", s."SiblingValue" +FROM "BaseEntity" AS b +INNER JOIN "SiblingEntity" AS s ON b."Id" = s."Id" +"""); + } + + public override async Task Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", b."Discriminator", b."SiblingValue" +FROM "BaseEntity" AS b +WHERE b."Discriminator" = 'SiblingEntity' +"""); + } + + public override async Task Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT b."Id", b."BaseValue", s."SiblingValue" +FROM "BaseEntity" AS b +INNER JOIN "SiblingEntity" AS s ON b."Id" = s."Id" +"""); + } + + public override async Task Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(bool async) + { + await base.Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(async); + + AssertSql( + """ +SELECT s."Id", s."BaseValue", s."SiblingValue" +FROM "SiblingEntity" AS s +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tph_entity_owning_a_split_reference_on_base_without_table_sharing(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_base_without_table_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpt_entity_owning_a_split_reference_on_base_without_table_sharing(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_base_without_table_sharing(async); + + AssertSql(); + } + + public override async Task Tpc_entity_owning_a_split_reference_on_base_without_table_sharing(bool async) + { + await base.Tpc_entity_owning_a_split_reference_on_base_without_table_sharing(async); + + AssertSql( + """ +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 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" + FROM "MiddleEntity" AS m + 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" + FROM "LeafEntity" AS l +) 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" +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tph_entity_owning_a_split_reference_on_middle_without_table_sharing(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_middle_without_table_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpt_entity_owning_a_split_reference_on_middle_without_table_sharing(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_middle_without_table_sharing(async); + + AssertSql(); + } + + public override async Task Tpc_entity_owning_a_split_reference_on_middle_without_table_sharing(bool async) + { + await base.Tpc_entity_owning_a_split_reference_on_middle_without_table_sharing(async); + + AssertSql( + """ +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 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" + FROM "MiddleEntity" AS m + 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" + FROM "LeafEntity" AS l +) 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" +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tph_entity_owning_a_split_reference_on_leaf_without_table_sharing(bool async) + { + await base.Tph_entity_owning_a_split_reference_on_leaf_without_table_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpt_entity_owning_a_split_reference_on_leaf_without_table_sharing(bool async) + { + await base.Tpt_entity_owning_a_split_reference_on_leaf_without_table_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpc_entity_owning_a_split_reference_on_leaf_without_table_sharing(bool async) + { + await base.Tpc_entity_owning_a_split_reference_on_leaf_without_table_sharing(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tph_entity_owning_a_split_collection_on_base(bool async) + { + await base.Tph_entity_owning_a_split_collection_on_base(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpt_entity_owning_a_split_collection_on_base(bool async) + { + await base.Tpt_entity_owning_a_split_collection_on_base(async); + + AssertSql(); + } + + public override async Task Tpc_entity_owning_a_split_collection_on_base(bool async) + { + await base.Tpc_entity_owning_a_split_collection_on_base(async); + + AssertSql( + """ +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 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" + FROM "MiddleEntity" AS m + 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" + FROM "LeafEntity" AS l +) 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 s0 ON u."Id" = s0."BaseEntityId" +ORDER BY u."Id" NULLS FIRST, s0."BaseEntityId" NULLS FIRST +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tph_entity_owning_a_split_collection_on_middle(bool async) + { + await base.Tph_entity_owning_a_split_collection_on_middle(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpt_entity_owning_a_split_collection_on_middle(bool async) + { + await base.Tpt_entity_owning_a_split_collection_on_middle(async); + + AssertSql(); + } + + public override async Task Tpc_entity_owning_a_split_collection_on_middle(bool async) + { + await base.Tpc_entity_owning_a_split_collection_on_middle(async); + + AssertSql( + """ +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 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" + FROM "MiddleEntity" AS m + 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" + FROM "LeafEntity" AS l +) 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 s0 ON u."Id" = s0."MiddleEntityId" +ORDER BY u."Id" NULLS FIRST, s0."MiddleEntityId" NULLS FIRST +"""); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tph_entity_owning_a_split_collection_on_leaf(bool async) + { + await base.Tph_entity_owning_a_split_collection_on_leaf(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpt_entity_owning_a_split_collection_on_leaf(bool async) + { + await base.Tpt_entity_owning_a_split_collection_on_leaf(async); + + AssertSql(); + } + + [ConditionalTheory(Skip = "Issue29075")] + public override async Task Tpc_entity_owning_a_split_collection_on_leaf(bool async) + { + await base.Tpc_entity_owning_a_split_collection_on_leaf(async); + + AssertSql(); + } + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/FieldsOnlyLoadGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/FieldsOnlyLoadGaussDBTest.cs new file mode 100644 index 0000000000..7c630ed4cc --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/FieldsOnlyLoadGaussDBTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class FieldsOnlyLoadGaussDBTest(FieldsOnlyLoadGaussDBTest.FieldsOnlyLoadGaussDBFixture fixture) + : FieldsOnlyLoadTestBase(fixture) +{ + public class FieldsOnlyLoadGaussDBFixture : FieldsOnlyLoadFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/FiltersInheritanceQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/FiltersInheritanceQueryGaussDBTest.cs new file mode 100644 index 0000000000..67848f867e --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/FiltersInheritanceQueryGaussDBTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class FiltersInheritanceQueryGaussDBTest : FiltersInheritanceQueryTestBase +{ + // ReSharper disable once UnusedParameter.Local + public FiltersInheritanceQueryGaussDBTest(TPHFiltersInheritanceQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/FromSqlQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/FromSqlQueryGaussDBTest.cs new file mode 100644 index 0000000000..741c880b05 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/FromSqlQueryGaussDBTest.cs @@ -0,0 +1,184 @@ +using System.Data.Common; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class FromSqlQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture) + : FromSqlQueryTestBase>(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); + + [ConditionalTheory(Skip = "https://github.com/aspnet/EntityFramework/issues/{6563,20364}")] + public override Task Bad_data_error_handling_invalid_cast_projection(bool async) + => base.Bad_data_error_handling_invalid_cast_projection(async); + + // 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) + { + // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match + // the database data. + var city = "London"; + var startDate = new GaussDBParameter { Value = new DateTime(1997, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + var endDate = new GaussDBParameter { Value = new DateTime(1998, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + await using var context = CreateContext(); + var query + = from c in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Set().FromSqlInterpolated( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) + where c.CustomerID == o.CustomerID + select new { c, o }; + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(25, actual.Length); + + city = "Berlin"; + startDate = new GaussDBParameter { Value = new DateTime(1998, 4, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + endDate = new GaussDBParameter { Value = new DateTime(1998, 5, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + query + = (from c in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Set().FromSqlInterpolated( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) + where c.CustomerID == o.CustomerID + select new { c, o }); + + actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Single(actual); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task FromSql_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(bool async) + { + // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match + // the database data. + var city = "London"; + var startDate = new GaussDBParameter { Value = new DateTime(1997, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + var endDate = new GaussDBParameter { Value = new DateTime(1998, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + await using var context = CreateContext(); + var query + = from c in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Set().FromSqlInterpolated( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) + where c.CustomerID == o.CustomerID + select new { c, o }; + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(25, actual.Length); + + city = "Berlin"; + startDate = new GaussDBParameter { Value = new DateTime(1998, 4, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + endDate = new GaussDBParameter { Value = new DateTime(1998, 5, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + query + = (from c in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Set().FromSqlInterpolated( + NormalizeDelimitersInInterpolatedString( + $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) + where c.CustomerID == o.CustomerID + select new { c, o }); + + actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Single(actual); + } + + public override async Task FromSqlRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) + { + // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match + // the database data. + var city = "London"; + var startDate = new GaussDBParameter { Value = new DateTime(1997, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + var endDate = new GaussDBParameter { Value = new DateTime(1998, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + await using var context = CreateContext(); + var query = from c in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), + startDate, + endDate) + where c.CustomerID == o.CustomerID + select new { c, o }; + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(25, actual.Length); + + city = "Berlin"; + startDate = new GaussDBParameter { Value = new DateTime(1998, 4, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + endDate = new GaussDBParameter { Value = new DateTime(1998, 5, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + query = (from c in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) + from o in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), + startDate, + endDate) + where c.CustomerID == o.CustomerID + select new { c, o }); + + actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Single(actual); + } + + public override async Task FromSqlRaw_queryable_multiple_composed_with_closure_parameters(bool async) + { + // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match + // the database data. + var startDate = new GaussDBParameter { Value = new DateTime(1997, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + var endDate = new GaussDBParameter { Value = new DateTime(1998, 1, 1), GaussDBDbType = GaussDBDbType.Timestamp }; + + await using var context = CreateContext(); + var query = from c in context.Set().FromSqlRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) + from o in context.Set().FromSqlRaw( + NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), + startDate, + endDate) + where c.CustomerID == o.CustomerID + select new { c, o }; + + var actual = async + ? await query.ToArrayAsync() + : query.ToArray(); + + Assert.Equal(411, actual.Length); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new GaussDBParameter { ParameterName = name, Value = value }; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/FullTextSearchDbFunctionsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/FullTextSearchDbFunctionsGaussDBTest.cs new file mode 100644 index 0000000000..683738cfb4 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/FullTextSearchDbFunctionsGaussDBTest.cs @@ -0,0 +1,1051 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +#pragma warning disable CS0618 // GaussDBTsVector.Parse is obsolete + +public class FullTextSearchDbFunctionsGaussDBTest : IClassFixture> +{ + protected NorthwindQueryGaussDBFixture Fixture { get; } + + // ReSharper disable once UnusedParameter.Local + public FullTextSearchDbFunctionsGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [Fact] + public void TsVectorParse_converted_to_cast() + { + using var context = CreateContext(); + var tsvector = context.Customers.Select(c => GaussDBTsVector.Parse("a b")).First(); + + Assert.NotNull(tsvector); + AssertSql( + """ +SELECT 'a b'::tsvector +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ArrayToTsVector_constants() + { + using var context = CreateContext(); + var tsvector = context.Customers.Select(c => EF.Functions.ArrayToTsVector(new[] { "b", "c", "d" })) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("b c d").ToString(), tsvector.ToString()); + AssertSql( + """ +SELECT array_to_tsvector(ARRAY['b','c','d']::text[]) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ArrayToTsVector_columns() + { + using var context = CreateContext(); + var tsvector = context.Customers + .OrderBy(c => c.CustomerID) + .Select(c => EF.Functions.ArrayToTsVector(new[] { c.CompanyName, c.Address })) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("'Alfreds Futterkiste' 'Obere Str. 57'").ToString(), tsvector.ToString()); + AssertSql( + """ +SELECT array_to_tsvector(ARRAY[c."CompanyName",c."Address"]::character varying(60)[]) +FROM "Customers" AS c +ORDER BY c."CustomerID" NULLS FIRST +LIMIT 1 +"""); + } + + [Fact] + public void ToTsVector() + { + using var context = CreateContext(); + var tsvector = context.Customers.Select(c => EF.Functions.ToTsVector(c.CompanyName)).First(); + + Assert.NotNull(tsvector); + AssertSql( + """ +SELECT to_tsvector(c."CompanyName") +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToTsVector_with_constant_config() + { + using var context = CreateContext(); + var tsvector = context.Customers.Select(c => EF.Functions.ToTsVector("english", c.CompanyName)).First(); + + Assert.NotNull(tsvector); + AssertSql( + """ +SELECT to_tsvector('english', c."CompanyName") +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToTsVector_with_parameter_config() + { + using var context = CreateContext(); + var config = "english"; + var tsvector = context.Customers.Select(c => EF.Functions.ToTsVector(config, c.CompanyName)).First(); + + Assert.NotNull(tsvector); + AssertSql( + """ +SELECT to_tsvector('english', c."CompanyName") +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsQueryParse_converted_to_cast() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => GaussDBTsQuery.Parse("a & b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT 'a & b'::tsquery +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void PlainToTsQuery() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.PlainToTsQuery("a")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT plainto_tsquery('a') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void PlainToTsQuery_with_constant_config() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.PlainToTsQuery("english", "a")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT plainto_tsquery('english', 'a') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void PlainToTsQuery_with_parameter_config() + { + using var context = CreateContext(); + var config = "english"; + var tsquery = context.Customers.Select(c => EF.Functions.PlainToTsQuery(config, "a")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT plainto_tsquery('english', 'a') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void PhraseToTsQuery() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.PhraseToTsQuery("a b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT phraseto_tsquery('a b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void PhraseToTsQuery_with_constant_config() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.PhraseToTsQuery("english", "a b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT phraseto_tsquery('english', 'a b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void PhraseToTsQuery_with_parameter_config() + { + using var context = CreateContext(); + var config = "english"; + var tsquery = context.Customers.Select(c => EF.Functions.PhraseToTsQuery(config, "a b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT phraseto_tsquery('english', 'a b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToTsQuery() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.ToTsQuery("a & b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT to_tsquery('a & b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToTsQuery_with_constant_config() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.ToTsQuery("english", "a & b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT to_tsquery('english', 'a & b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToTsQuery_with_parameter_config() + { + using var context = CreateContext(); + var config = "english"; + var tsquery = context.Customers.Select(c => EF.Functions.ToTsQuery(config, "a & b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT to_tsquery('english', 'a & b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(11, 0)] + public void WebSearchToTsQuery() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.WebSearchToTsQuery("a OR b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT websearch_to_tsquery('a OR b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(11, 0)] + public void WebSearchToTsQuery_with_constant_config() + { + using var context = CreateContext(); + var tsquery = context.Customers.Select(c => EF.Functions.WebSearchToTsQuery("english", "a OR b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT websearch_to_tsquery('english', 'a OR b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(11, 0)] + public void WebSearchToTsQuery_with_parameter_config() + { + using var context = CreateContext(); + var config = "english"; + var tsquery = context.Customers.Select(c => EF.Functions.WebSearchToTsQuery(config, "a OR b")).First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT websearch_to_tsquery('english', 'a OR b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsQuery_and() + { + using var context = CreateContext(); + var tsquery = context.Customers + .Select(c => EF.Functions.ToTsQuery("a & b").And(EF.Functions.ToTsQuery("c & d"))) + .First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT to_tsquery('a & b') && to_tsquery('c & d') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsQuery_or() + { + using var context = CreateContext(); + var tsquery = context.Customers + .Select(c => EF.Functions.ToTsQuery("a & b").Or(EF.Functions.ToTsQuery("c & d"))) + .First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT to_tsquery('a & b') || to_tsquery('c & d') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsQuery_ToNegative() + { + using var context = CreateContext(); + var tsquery = context.Customers + .Select(c => EF.Functions.ToTsQuery("a & b").ToNegative()) + .First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT !!to_tsquery('a & b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsQuery_Contains() + { + using var context = CreateContext(); + var result = context.Customers + .Select(c => EF.Functions.ToTsQuery("a & b").Contains(EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.True(result); + AssertSql( + """ +SELECT to_tsquery('a & b') @> to_tsquery('b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsQuery_IsContainedIn() + { + using var context = CreateContext(); + var result = context.Customers + .Select(c => EF.Functions.ToTsQuery("b").IsContainedIn(EF.Functions.ToTsQuery("a & b"))) + .First(); + + Assert.True(result); + AssertSql( + """ +SELECT to_tsquery('b') <@ to_tsquery('a & b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetNodeCount() + { + using var context = CreateContext(); + var nodeCount = context.Customers + .Select(c => EF.Functions.ToTsQuery("b").GetNodeCount()) + .First(); + + Assert.Equal(1, nodeCount); + AssertSql( + """ +SELECT numnode(to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetQueryTree() + { + using var context = CreateContext(); + var queryTree = context.Customers + .Select(c => EF.Functions.ToTsQuery("b").GetQueryTree()) + .First(); + + Assert.NotEmpty(queryTree); + AssertSql( + """ +SELECT querytree(to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetResultHeadline() + { + using var context = CreateContext(); + var headline = context.Customers + .Select(c => EF.Functions.ToTsQuery("b").GetResultHeadline("a b c")) + .First(); + + Assert.NotEmpty(headline); + AssertSql( + """ +SELECT ts_headline('a b c', to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetResultHeadline_with_options() + { + using var context = CreateContext(); + var headline = context.Customers + .Select(c => EF.Functions.ToTsQuery("b").GetResultHeadline("a b c", "MinWords=1, MaxWords=2")) + .First(); + + Assert.NotEmpty(headline); + AssertSql( + """ +SELECT ts_headline('a b c', to_tsquery('b'), 'MinWords=1, MaxWords=2') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetResultHeadline_with_constant_config_and_options() + { + using var context = CreateContext(); + var headline = context.Customers + .Select( + c => EF.Functions.ToTsQuery("b").GetResultHeadline( + "english", + "a b c", + "MinWords=1, MaxWords=2")) + .First(); + + Assert.NotEmpty(headline); + AssertSql( + """ +SELECT ts_headline('english', 'a b c', to_tsquery('b'), 'MinWords=1, MaxWords=2') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetResultHeadline_with_parameter_config_and_options() + { + using var context = CreateContext(); + var config = "english"; + var headline = context.Customers + .Select( + c => EF.Functions.ToTsQuery("b").GetResultHeadline( + config, + "a b c", + "MinWords=1, MaxWords=2")) + .First(); + + Assert.NotEmpty(headline); + AssertSql( + """ +@config='english' + +SELECT ts_headline(@config::regconfig, 'a b c', to_tsquery('b'), 'MinWords=1, MaxWords=2') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Rewrite() + { + using var context = CreateContext(); + var rewritten = context.Customers + .Select( + c => EF.Functions.ToTsQuery("a & b").Rewrite( + EF.Functions.ToTsQuery("b"), + EF.Functions.ToTsQuery("c"))) + .First(); + + Assert.NotNull(rewritten); + AssertSql( + """ +SELECT ts_rewrite(to_tsquery('a & b'), to_tsquery('b'), to_tsquery('c')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Rewrite_with_select() + { + using var context = CreateContext(); + var rewritten = context.Customers + .Select( + c => EF.Functions.ToTsQuery("a & b").Rewrite( + """SELECT 'a'::tsquery, 'c'::tsquery""")) + .First(); + + Assert.NotNull(rewritten); + AssertSql( + """ +SELECT ts_rewrite(to_tsquery('a & b'), 'SELECT ''a''::tsquery, ''c''::tsquery') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToPhrase() + { + using var context = CreateContext(); + var tsquery = context.Customers + .Select(c => EF.Functions.ToTsQuery("a").ToPhrase(EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT tsquery_phrase(to_tsquery('a'), to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToPhrase_with_distance() + { + using var context = CreateContext(); + var tsquery = context.Customers + .Select(c => EF.Functions.ToTsQuery("a").ToPhrase(EF.Functions.ToTsQuery("b"), 10)) + .First(); + + Assert.NotNull(tsquery); + AssertSql( + """ +SELECT tsquery_phrase(to_tsquery('a'), to_tsquery('b'), 10) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Matches_with_string() + { + using var context = CreateContext(); + var query = "b"; + var result = context.Customers + .Select(c => EF.Functions.ToTsVector("a").Matches(query)) + .First(); + + Assert.False(result); + AssertSql( + """ +@query='b' + +SELECT to_tsvector('a') @@ plainto_tsquery(@query) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Matches_with_TsQuery() + { + using var context = CreateContext(); + var result = context.Customers + .Select(c => EF.Functions.ToTsVector("a").Matches(EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.False(result); + AssertSql( + """ +SELECT to_tsvector('a') @@ to_tsquery('b') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void TsVector_Concat() + { + using var context = CreateContext(); + var tsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("b").Concat(EF.Functions.ToTsVector("c"))) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("b:1 c:2").ToString(), tsVector.ToString()); + AssertSql( + """ +SELECT to_tsvector('b') || to_tsvector('c') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void SetWeight_with_enum() + { + using var context = CreateContext(); + var weightedTsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("a").SetWeight(GaussDBTsVector.Lexeme.Weight.A)) + .First(); + + Assert.NotNull(weightedTsVector); + AssertSql( + """ +SELECT setweight(to_tsvector('a'), 'A') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void SetWeight_with_enum_and_lexemes() + { + using var context = CreateContext(); + var weightedTsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("a").SetWeight(GaussDBTsVector.Lexeme.Weight.A, new[] { "a" })) + .First(); + + Assert.NotNull(weightedTsVector); + AssertSql( + """ +SELECT setweight(to_tsvector('a'), 'A', ARRAY['a']::text[]) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void SetWeight_with_char() + { + using var context = CreateContext(); + var weightedTsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("a").SetWeight('A')) + .First(); + + Assert.NotNull(weightedTsVector); + AssertSql( + """ +SELECT setweight(to_tsvector('a'), 'A') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void SetWeight_with_char_and_lexemes() + { + using var context = CreateContext(); + var weightedTsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("a").SetWeight('A', new[] { "a" })) + .First(); + + Assert.NotNull(weightedTsVector); + AssertSql( + """ +SELECT setweight(to_tsvector('a'), 'A', ARRAY['a']::text[]) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Delete_with_single_lexeme() + { + using var context = CreateContext(); + var tsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("b c").Delete("c")) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("b:1").ToString(), tsVector.ToString()); + AssertSql( + """ +SELECT ts_delete(to_tsvector('b c'), 'c') +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Delete_with_multiple_lexemes() + { + using var context = CreateContext(); + var tsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("b c d").Delete(new[] { "c", "d" })) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("b:1").ToString(), tsVector.ToString()); + AssertSql( + """ +SELECT ts_delete(to_tsvector('b c d'), ARRAY['c','d']::text[]) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact(Skip = "Need to reimplement with \"char\"[]")] + public void Filter() + { + using var context = CreateContext(); + var tsVector = context.Customers + .Select(c => GaussDBTsVector.Parse("b:1A c:2B d:3C").Filter(new[] { 'B', 'C' })) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("c:2B d:3C").ToString(), tsVector.ToString()); + AssertSql( + """ +SELECT ts_filter(CAST('b:1A c:2B d:3C' AS tsvector), CAST(ARRAY['B','C']::character(1)[] AS "char"[])) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void GetLength() + { + using var context = CreateContext(); + var length = context.Customers + .Select(c => EF.Functions.ToTsVector("c").GetLength()) + .First(); + + Assert.Equal(1, length); + AssertSql( + """ +SELECT length(to_tsvector('c')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void ToStripped() + { + using var context = CreateContext(); + var strippedTsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("c:A").ToStripped()) + .First(); + + Assert.Equal(GaussDBTsVector.Parse("c").ToString(), strippedTsVector.ToString()); + AssertSql( + """ +SELECT strip(to_tsvector('c:A')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Rank() + { + using var context = CreateContext(); + var rank = context.Customers + .Select(c => EF.Functions.ToTsVector("a b c").Rank(EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank(to_tsvector('a b c'), to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Rank_with_normalization() + { + using var context = CreateContext(); + var rank = context.Customers + .Select( + c => EF.Functions.ToTsVector("a b c").Rank( + EF.Functions.ToTsQuery("b"), + GaussDBTsRankingNormalization.DivideByLength)) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank(to_tsvector('a b c'), to_tsquery('b'), 2) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Rank_with_weights() + { + using var context = CreateContext(); + var rank = context.Customers + .Select( + c => EF.Functions.ToTsVector("a b c").Rank( + new float[] { 1, 1, 1, 1 }, + EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Rank_with_weights_and_normalization() + { + using var context = CreateContext(); + var rank = context.Customers + .Select( + c => EF.Functions.ToTsVector("a b c").Rank( + new float[] { 1, 1, 1, 1 }, + EF.Functions.ToTsQuery("b"), + GaussDBTsRankingNormalization.DivideByLength)) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b'), 2) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void RankCoverDensity() + { + using var context = CreateContext(); + var rank = context.Customers + .Select(c => EF.Functions.ToTsVector("a b c").RankCoverDensity(EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank_cd(to_tsvector('a b c'), to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void RankCoverDensity_with_normalization() + { + using var context = CreateContext(); + var rank = context.Customers + .Select( + c => EF.Functions.ToTsVector("a b c").RankCoverDensity( + EF.Functions.ToTsQuery("b"), + GaussDBTsRankingNormalization.DivideByLength)) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank_cd(to_tsvector('a b c'), to_tsquery('b'), 2) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void RankCoverDensity_with_weights() + { + using var context = CreateContext(); + var rank = context.Customers + .Select( + c => EF.Functions.ToTsVector("a b c").RankCoverDensity( + new float[] { 1, 1, 1, 1 }, + EF.Functions.ToTsQuery("b"))) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank_cd(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b')) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void RankCoverDensity_with_weights_and_normalization() + { + using var context = CreateContext(); + var rank = context.Customers + .Select( + c => EF.Functions.ToTsVector("a b c").RankCoverDensity( + new float[] { 1, 1, 1, 1 }, + EF.Functions.ToTsQuery("b"), + GaussDBTsRankingNormalization.DivideByLength)) + .First(); + + Assert.True(rank > 0); + AssertSql( + """ +SELECT ts_rank_cd(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b'), 2) +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Basic_where() + { + using var context = CreateContext(); + var count = context.Customers + .Count(c => EF.Functions.ToTsVector(c.ContactTitle).Matches(EF.Functions.ToTsQuery("owner"))); + + Assert.True(count > 0); + } + + [Fact] + public void Complex_query() + { + using var context = CreateContext(); + var headline = context.Customers + .Where( + c => EF.Functions.ToTsVector(c.ContactTitle) + .SetWeight(GaussDBTsVector.Lexeme.Weight.A) + .Matches(EF.Functions.ToTsQuery("accounting").ToPhrase(EF.Functions.ToTsQuery("manager")))) + .Select( + c => EF.Functions.ToTsQuery("accounting").ToPhrase(EF.Functions.ToTsQuery("manager")) + .GetResultHeadline(c.ContactTitle)) + .First(); + + Assert.Equal("Accounting Manager", headline); + } + + [Fact] + public void Unaccent() + { + using var context = CreateContext(); + _ = context.Customers + .Select(x => EF.Functions.Unaccent(x.ContactName)) + .FirstOrDefault(); + + AssertSql( + """ +SELECT unaccent(c."ContactName") +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Unaccent_with_constant_regdictionary() + { + using var context = CreateContext(); + _ = context.Customers + .Select(x => EF.Functions.Unaccent("unaccent", x.ContactName)) + .FirstOrDefault(); + + AssertSql( + """ +SELECT unaccent('unaccent', c."ContactName") +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] + public void Unaccent_with_parameter_regdictionary() + { + using var context = CreateContext(); + var regDictionary = "unaccent"; + _ = context.Customers + .Select(x => EF.Functions.Unaccent(regDictionary, x.ContactName)) + .FirstOrDefault(); + + AssertSql( + """ +@regDictionary='unaccent' + +SELECT unaccent(@regDictionary::regdictionary, c."ContactName") +FROM "Customers" AS c +LIMIT 1 +"""); + } + + [Fact] // #1652 + public void Match_and_boolean_operator_precedence() + { + using var context = CreateContext(); + _ = context.Customers + .Count( + c => EF.Functions.ToTsVector(c.ContactTitle) + .Matches(EF.Functions.ToTsQuery("owner").Or(EF.Functions.ToTsQuery("foo")))); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE to_tsvector(c."ContactTitle") @@ (to_tsquery('owner') || to_tsquery('foo')) +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected NorthwindContext CreateContext() + => Fixture.CreateContext(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/FunkyDataQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/FunkyDataQueryGaussDBTest.cs new file mode 100644 index 0000000000..b776150bc8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/FunkyDataQueryGaussDBTest.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore.TestModels.FunkyDataModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class FunkyDataQueryGaussDBTest : FunkyDataQueryTestBase +{ + // ReSharper disable once UnusedParameter.Local + public FunkyDataQueryGaussDBTest(FunkyDataQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override Task String_FirstOrDefault_and_LastOrDefault(bool async) + => Task.CompletedTask; // GaussDB doesn't support reading an empty string as a char at the ADO level + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task String_starts_with_on_argument_with_escape_constant(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName.StartsWith("Some\\")), + ss => ss.Set().Where(c => c.FirstName != null && c.FirstName.StartsWith("Some\\"))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task String_starts_with_on_argument_with_escape_parameter(bool async) + { + var param = "Some\\"; + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName.StartsWith(param)), + ss => ss.Set().Where(c => c.FirstName != null && c.FirstName.StartsWith(param))); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class FunkyDataQueryGaussDBFixture : FunkyDataQueryFixtureBase, ITestSqlLoggerFactory + { + private FunkyDataData? _expectedData; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override FunkyDataContext CreateContext() + { + var context = base.CreateContext(); + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + return context; + } + + public override ISetSource GetExpectedData() + { + if (_expectedData is null) + { + _expectedData = (FunkyDataData)base.GetExpectedData(); + + var maxId = _expectedData.FunkyCustomers.Max(c => c.Id); + + var mutableCustomersOhYeah = (List)_expectedData.FunkyCustomers; + + mutableCustomersOhYeah.Add( + new FunkyCustomer + { + Id = maxId + 1, + FirstName = "Some\\Guy", + LastName = null + }); + } + + return _expectedData; + } + + protected override async Task SeedAsync(FunkyDataContext context) + { + context.FunkyCustomers.AddRange(GetExpectedData().Set()); + await context.SaveChangesAsync(); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/FuzzyStringMatchQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/FuzzyStringMatchQueryGaussDBTest.cs new file mode 100644 index 0000000000..2c260e044d --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/FuzzyStringMatchQueryGaussDBTest.cs @@ -0,0 +1,223 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// Provides unit tests for the fuzzystrmatch module function translations. +/// +/// +/// See: https://www.postgresql.org/docs/current/fuzzystrmatch.html +/// +public class FuzzyStringMatchQueryGaussDBTest : IClassFixture +{ + private FuzzyStringMatchQueryGaussDBFixture Fixture { get; } + + // ReSharper disable once UnusedParameter.Local + public FuzzyStringMatchQueryGaussDBTest(FuzzyStringMatchQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region FunctionTests + + [Fact] + public void FuzzyStringMatchSoundex() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchSoundex(x.Text)) + .ToArray(); + + AssertContainsSql("""soundex(f."Text")"""); + } + + [Fact] + public void FuzzyStringMatchDifference() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchDifference(x.Text, "target")) + .ToArray(); + + AssertContainsSql("""difference(f."Text", 'target')"""); + } + + [Fact] + public void FuzzyStringMatchLevenshtein() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target")) + .ToArray(); + + AssertContainsSql("""levenshtein(f."Text", 'target')"""); + } + + [Fact] + public void FuzzyStringMatchLevenshtein_With_Costs() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target", 1, 2, 3)) + .ToArray(); + + AssertContainsSql("""levenshtein(f."Text", 'target', 1, 2, 3)"""); + } + + [Fact] + public void FuzzyStringMatchLevenshteinLessEqual() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 5)) + .ToArray(); + + AssertContainsSql("""levenshtein_less_equal(f."Text", 'target', 5)"""); + } + + [Fact] + public void FuzzyStringMatchLevenshteinLessEqual_With_Costs() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 1, 2, 3, 5)) + .ToArray(); + + AssertContainsSql("""levenshtein_less_equal(f."Text", 'target', 1, 2, 3, 5)"""); + } + + [Fact] + public void FuzzyStringMatchMetaphone() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchMetaphone(x.Text, 6)) + .ToArray(); + + AssertContainsSql("""metaphone(f."Text", 6)"""); + } + + [Fact] + public void FuzzyStringMatchDoubleMetaphone() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphone(x.Text)) + .ToArray(); + + AssertContainsSql("""dmetaphone(f."Text")"""); + } + + [Fact] + public void FuzzyStringMatchDoubleMetaphoneAlt() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphoneAlt(x.Text)) + .ToArray(); + + AssertContainsSql("""dmetaphone_alt(f."Text")"""); + } + + #endregion + + #region Fixtures + + /// + /// Represents a fixture suitable for testing fuzzy string match functions. + /// + public class FuzzyStringMatchQueryGaussDBFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "FuzzyStringMatchQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override Task SeedAsync(FuzzyStringMatchContext context) + => FuzzyStringMatchContext.SeedAsync(context); + } + + /// + /// Represents an entity suitable for testing fuzzy string match functions. + /// + public class FuzzyStringMatchTestEntity + { + // ReSharper disable once UnusedMember.Global + /// + /// The primary key. + /// + [Key] + public int Id { get; set; } + + /// + /// Some text. + /// + public string Text { get; set; } = null!; + } + + /// + /// Represents a database suitable for testing fuzzy string match functions. + /// + public class FuzzyStringMatchContext : PoolableDbContext + { + /// + /// Represents a set of entities with properties. + /// + public DbSet FuzzyStringMatchTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public FuzzyStringMatchContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresExtension("fuzzystrmatch"); + + base.OnModelCreating(modelBuilder); + } + + public static async Task SeedAsync(FuzzyStringMatchContext context) + { + for (var i = 1; i <= 9; i++) + { + var text = "Some text " + i; + context.FuzzyStringMatchTestEntities.Add( + new FuzzyStringMatchTestEntity { Id = i, Text = text }); + } + + await context.SaveChangesAsync(); + } + } + + #endregion + + #region Helpers + + protected FuzzyStringMatchContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. + private void AssertContainsSql(string sql) + => Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); + + #endregion +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarFromSqlQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarFromSqlQueryGaussDBTest.cs new file mode 100644 index 0000000000..711c42dd55 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarFromSqlQueryGaussDBTest.cs @@ -0,0 +1,28 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class GearsOfWarFromSqlQueryGaussDBTest : GearsOfWarFromSqlQueryTestBase +{ + // ReSharper disable once UnusedParameter.Local + public GearsOfWarFromSqlQueryGaussDBTest(GearsOfWarQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + 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" +""", + Sql); + } + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + private string Sql + => Fixture.TestSqlLoggerFactory.Sql; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarQueryGaussDBFixture.cs new file mode 100644 index 0000000000..a7d728b926 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarQueryGaussDBFixture.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query; + + +public class GearsOfWarQueryGaussDBFixture : GearsOfWarQueryRelationalFixture +{ + protected override string StoreName + => "GearsOfWarQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + private GearsOfWarData? _expectedData; + + static GearsOfWarQueryGaussDBFixture() + { + // TODO: Switch to using GaussDBDataSource +#pragma warning disable CS0618 // Type or member is obsolete + GaussDBConnection.GlobalTypeMapper.EnableRecordsAsTuples(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("uuid-ossp"); + + modelBuilder.Entity().Property(c => c.IssueDate).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); + } + + public override ISetSource GetExpectedData() + { + if (_expectedData is null) + { + _expectedData = (GearsOfWarData)base.GetExpectedData(); + + // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. + // Also chop sub-microsecond precision which GaussDB does not support. + foreach (var mission in _expectedData.Missions) + { + mission.Timeline = new DateTimeOffset( + mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + mission.Duration = new TimeSpan( + mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); + } + } + + return _expectedData; + } + + 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 GaussDB does not support. + // See https://github.com/dotnet/efcore/issues/26068 + + var squads = GearsOfWarData.CreateSquads(); + var missions = GearsOfWarData.CreateMissions(); + var squadMissions = GearsOfWarData.CreateSquadMissions(); + var cities = GearsOfWarData.CreateCities(); + var weapons = GearsOfWarData.CreateWeapons(); + var tags = GearsOfWarData.CreateTags(); + var gears = GearsOfWarData.CreateGears(); + var locustLeaders = GearsOfWarData.CreateLocustLeaders(); + var factions = GearsOfWarData.CreateFactions(); + var locustHighCommands = GearsOfWarData.CreateHighCommands(); + + foreach (var mission in missions) + { + mission.Timeline = new DateTimeOffset( + mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + mission.Duration = new TimeSpan( + mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); + } + + GearsOfWarData.WireUp( + squads, missions, squadMissions, cities, weapons, tags, gears, locustLeaders, factions, locustHighCommands); + + context.Squads.AddRange(squads); + context.Missions.AddRange(missions); + context.SquadMissions.AddRange(squadMissions); + context.Cities.AddRange(cities); + context.Weapons.AddRange(weapons); + context.Tags.AddRange(tags); + context.Gears.AddRange(gears); + context.LocustLeaders.AddRange(locustLeaders); + context.Factions.AddRange(factions); + context.LocustHighCommands.AddRange(locustHighCommands); + await context.SaveChangesAsync(); + + GearsOfWarData.WireUp2(locustLeaders, factions); + + await context.SaveChangesAsync(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarQueryGaussDBTest.cs new file mode 100644 index 0000000000..a3a6b61268 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/GearsOfWarQueryGaussDBTest.cs @@ -0,0 +1,236 @@ +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +// ReSharper disable once UnusedMember.Global +public class GearsOfWarQueryGaussDBTest : GearsOfWarQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public GearsOfWarQueryGaussDBTest(GearsOfWarQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Byte array + + public override async Task Byte_array_filter_by_length_literal_does_not_cast_on_varbinary_n(bool async) + { + await base.Byte_array_filter_by_length_literal_does_not_cast_on_varbinary_n(async); + + AssertSql( + """ +SELECT s."Id", s."Banner", s."Banner5", s."InternalNumber", s."Name" +FROM "Squads" AS s +WHERE length(s."Banner5") = 5 +"""); + } + + #endregion Byte array + + #region DateTimeOffset + + // Not supported by design: we support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a + // non-UTC DateTimeOffset. + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => Assert.ThrowsAsync(() => base.DateTimeOffsetNow_minus_timespan(async)); + + public override async Task DateTimeOffset_Date_returns_datetime(bool async) + { + var dateTimeOffset = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Timeline.Date.ToLocalTime() >= dateTimeOffset.Date)); + + AssertSql( + """ +@dateTimeOffset_Date='0002-03-01T00:00:00.0000000' + +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 +"""); + } + + public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) + { + var dto = new DateTimeOffset(599898024001234567, new TimeSpan(0)); + var start = dto.AddDays(-1); + var end = dto.AddDays(1); + var dates = new[] { dto }; + + await AssertQuery( + async, + ss => ss.Set().Where( + m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline)), + assertEmpty: true); // TODO: Look into this + + AssertSql( + """ +@start='1902-01-01T10:00:00.1234567+00:00' (DbType = DateTime) +@end='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) +@dates={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) + +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" +FROM "Missions" AS m +WHERE @start <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @end AND m."Timeline" = ANY (@dates) +"""); + } + + // Base implementation uses DateTimeOffset.Now, which we don't translate by design. Use DateTimeOffset.UtcNow instead. + public override async Task Select_datetimeoffset_comparison_in_projection(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set().Select(m => m.Timeline > DateTimeOffset.UtcNow)); + + AssertSql( + """ +SELECT m."Timeline" > now() +FROM "Missions" AS m +"""); + } + + #endregion DateTimeOffset + + #region DateTime + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_datetime_subtraction(bool async) + => AssertQuery( + async, + ss => ss.Set().Where( + m => + new DateTimeOffset(2, 3, 2, 8, 0, 0, new TimeSpan(-5, 0, 0)) - m.Timeline > TimeSpan.FromDays(3)), + assertEmpty: true); // TODO: Look into this + + #endregion DateTime + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Where_TimeSpan_TotalDays(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Duration.TotalDays < 0.042)); + + AssertSql( + """ +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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Where_TimeSpan_TotalHours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Duration.TotalHours < 1.02)); + + AssertSql( + """ +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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Where_TimeSpan_TotalMinutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Duration.TotalMinutes < 61)); + + AssertSql( + """ +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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Where_TimeSpan_TotalSeconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Duration.TotalSeconds < 3700)); + + AssertSql( + """ +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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Where_TimeSpan_TotalMilliseconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Duration.TotalMilliseconds < 3700000)); + + AssertSql( + """ +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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_Property_Select_Sum_over_TimeSpan(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Sum(g.Select(o => o.Duration))), + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => (TimeSpan?)new TimeSpan(g.Sum(o => o.Duration.Ticks)))); + + AssertSql( + """ +SELECT sum(m."Duration") +FROM "Missions" AS m +GROUP BY m."Id" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_Property_Select_Average_over_TimeSpan(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Average(g.Select(o => o.Duration))), + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => (TimeSpan?)new TimeSpan((long)g.Average(o => o.Duration.Ticks)))); + + AssertSql( + """ +SELECT avg(m."Duration") +FROM "Missions" AS m +GROUP BY m."Id" +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/IncludeGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/IncludeGaussDBFixture.cs new file mode 100644 index 0000000000..b21ea88798 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/IncludeGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class IncludeGaussDBFixture : NorthwindQueryGaussDBFixture +{ + protected override bool ShouldLogCategory(string logCategory) + => base.ShouldLogCategory(logCategory) || logCategory == DbLoggerCategory.Query.Name; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/IncludeOneToOneGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/IncludeOneToOneGaussDBTest.cs new file mode 100644 index 0000000000..abab082ef2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/IncludeOneToOneGaussDBTest.cs @@ -0,0 +1,21 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +// ReSharper disable once UnusedMember.Global +public class IncludeOneToOneGaussDBTest : IncludeOneToOneTestBase +{ + // ReSharper disable once UnusedParameter.Local + public IncludeOneToOneGaussDBTest(OneToOneQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public class OneToOneQueryGaussDBFixture : OneToOneQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/InheritanceRelationshipsQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/InheritanceRelationshipsQueryGaussDBFixture.cs new file mode 100644 index 0000000000..84659e582d --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/InheritanceRelationshipsQueryGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class InheritanceRelationshipsQueryGaussDBFixture : InheritanceRelationshipsQueryRelationalFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/InheritanceRelationshipsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/InheritanceRelationshipsQueryGaussDBTest.cs new file mode 100644 index 0000000000..4b46254015 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/InheritanceRelationshipsQueryGaussDBTest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class InheritanceRelationshipsQueryGaussDBTest(InheritanceRelationshipsQueryGaussDBFixture fixture) + : InheritanceRelationshipsQueryTestBase(fixture) +{ + protected override void ClearLog() { } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/JsonDomQueryTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/JsonDomQueryTest.cs index cfb8de9089..70cec7f4d7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonDomQueryTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/JsonDomQueryTest.cs @@ -764,7 +764,7 @@ protected override string StoreName => "JsonDomQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/JsonPocoQueryTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/JsonPocoQueryTest.cs index ffc1ae259b..5d1b5b374e 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonPocoQueryTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/JsonPocoQueryTest.cs @@ -320,7 +320,7 @@ public void Array_toplevel() """ SELECT j."Id", j."Customer", j."ToplevelArray" FROM "JsonbEntities" AS j -WHERE (j."ToplevelArray" ->> 1) = 'two' +WHERE j."ToplevelArray" ->> 1 = 'two' LIMIT 2 """); } @@ -846,9 +846,9 @@ public class JsonPocoQueryFixture : SharedStoreFixtureBase { static JsonPocoQueryFixture() { - // TODO: Switch to using NpgsqlDataSource + // TODO: Switch to using GaussDBDataSource #pragma warning disable CS0618 // Type or member is obsolete - NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson(); + GaussDBConnection.GlobalTypeMapper.EnableDynamicJson(); #pragma warning restore CS0618 // Type or member is obsolete } @@ -856,7 +856,7 @@ protected override string StoreName => "JsonPocoQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/JsonQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/JsonQueryGaussDBTest.cs new file mode 100644 index 0000000000..f0e5692e11 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/JsonQueryGaussDBTest.cs @@ -0,0 +1,3431 @@ +using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class JsonQueryGaussDBTest : JsonQueryRelationalTestBase +{ + public JsonQueryGaussDBTest(JsonQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + 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_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_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); + + AssertSql( + ); + } + + public override async Task Project_json_collection_in_tracking_query_fails(bool async) + { + await base.Project_json_collection_in_tracking_query_fails(async); + + AssertSql( + ); + } + + public override async Task Project_json_entity_in_tracking_query_fails_even_when_owner_is_present(bool async) + { + await base.Project_json_entity_in_tracking_query_fails_even_when_owner_is_present(async); + + AssertSql( + ); + } + + 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_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_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_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_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_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); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Basic_json_projection_owned_collection_leaf(bool async) + { + await base.Basic_json_projection_owned_collection_leaf(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Basic_json_projection_scalar(bool async) + { + await base.Basic_json_projection_scalar(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" ->> 'Name' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_scalar_length(bool async) + { + await base.Json_scalar_length(async); + + AssertSql( + """ +SELECT j."Name" +FROM "JsonEntitiesBasic" AS j +WHERE length(j."OwnedReferenceRoot" ->> 'Name')::int > 2 +"""); + } + + public override async Task Basic_json_projection_enum_inside_json_entity(bool async) + { + await base.Basic_json_projection_enum_inside_json_entity(async); + + AssertSql( + """ +SELECT j."Id", CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Enum}' AS integer) AS "Enum" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_enum_with_custom_conversion(bool async) + { + await base.Json_projection_enum_with_custom_conversion(async); + + AssertSql( + """ +SELECT j."Id", CAST(j.json_reference_custom_naming ->> '1CustomEnum' AS integer) AS "Enum" +FROM "JsonEntitiesCustomNaming" AS j +"""); + } + + public override async Task Json_property_in_predicate(bool async) + { + await base.Json_property_in_predicate(async); + + AssertSql( + """ +SELECT j."Id" +FROM "JsonEntitiesBasic" AS j +WHERE (CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Fraction}' AS numeric(18,2))) < 20.5 +"""); + } + + public override async Task Json_subquery_property_pushdown_length(bool async) + { + await base.Json_subquery_property_pushdown_length(async); + + AssertSql( + """ +@p='3' + +SELECT length(j1.c)::int +FROM ( + 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 + ) AS j0 +) AS j1 +"""); + } + + public override async Task Json_subquery_reference_pushdown_reference(bool async) + { + await base.Json_subquery_reference_pushdown_reference(async); + + AssertSql( + """ +@p='10' + +SELECT j1.c -> 'OwnedReferenceBranch', j1."Id" +FROM ( + 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 + ) AS j0 +) AS j1 +"""); + } + + public override async Task Json_subquery_reference_pushdown_reference_anonymous_projection(bool async) + { + await base.Json_subquery_reference_pushdown_reference_anonymous_projection(async); + + AssertSql( + """ +@__p_0='10' + +SELECT JSON_QUERY([t0].[c], '$.OwnedReferenceSharedBranch'), [t0].[Id], CAST(LEN([t0].[c0]) AS int) +FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[c0] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[json_reference_shared], '$') AS [c], [j].[Id], CAST(JSON_VALUE([j].[json_reference_shared], '$.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf.SomethingSomething') AS nvarchar(max)) AS [c0] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] +) AS [t0] +"""); + } + + public override async Task Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(bool async) + { + await base.Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(async); + + AssertSql( + """ +@__p_0='10' + +SELECT JSON_QUERY([t2].[c],'$.OwnedReferenceSharedLeaf'), [t2].[Id], JSON_QUERY([t2].[c], '$.OwnedCollectionSharedLeaf'), [t2].[Length] +FROM ( + SELECT DISTINCT JSON_QUERY([t1].[c],'$') AS [c], [t1].[Id], [t1].[Length] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([t0].[c], '$.OwnedReferenceSharedBranch') AS [c], [t0].[Id], CAST(LEN([t0].[Scalar]) AS int) AS [Length] + FROM ( + SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[Scalar] + FROM ( + SELECT TOP(@__p_0) JSON_QUERY([j].[json_reference_shared], '$') AS [c], [j].[Id], CAST(JSON_VALUE([j].[json_reference_shared], '$.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf.SomethingSomething') AS nvarchar(max)) AS [Scalar] + FROM [JsonEntitiesBasic] AS [j] + ORDER BY [j].[Id] + ) AS [t] + ) AS [t0] + ORDER BY CAST(LEN([t0].[Scalar]) AS int) + ) AS [t1] +) AS [t2] +"""); + } + + public override async Task Json_subquery_reference_pushdown_reference_pushdown_reference(bool async) + { + await base.Json_subquery_reference_pushdown_reference_pushdown_reference(async); + + AssertSql( + """ +@p='10' + +SELECT j3.c -> 'OwnedReferenceLeaf', j3."Id" +FROM ( + SELECT DISTINCT j2.c AS c, j2."Id" + FROM ( + SELECT j1.c -> 'OwnedReferenceBranch' AS c, j1."Id" + FROM ( + 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 + ) AS j0 + ) AS j1 + ORDER BY j1.c0 ->> 'Name' NULLS FIRST + LIMIT @p + ) AS j2 +) AS j3 +"""); + } + + public override async Task Json_subquery_reference_pushdown_reference_pushdown_collection(bool async) + { + await base.Json_subquery_reference_pushdown_reference_pushdown_collection(async); + + AssertSql( + """ +@p='10' + +SELECT j3.c -> 'OwnedCollectionLeaf', j3."Id" +FROM ( + SELECT DISTINCT j2.c AS c, j2."Id" + FROM ( + SELECT j1.c -> 'OwnedReferenceBranch' AS c, j1."Id" + FROM ( + 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 + ) AS j0 + ) AS j1 + ORDER BY j1.c0 ->> 'Name' NULLS FIRST + LIMIT @p + ) AS j2 +) AS j3 +"""); + } + + public override async Task Json_subquery_reference_pushdown_property(bool async) + { + await base.Json_subquery_reference_pushdown_property(async); + + AssertSql( + """ +@p='10' + +SELECT j1.c ->> 'SomethingSomething' +FROM ( + 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 + ) AS j0 +) AS j1 +"""); + } + + public override async Task Custom_naming_projection_owner_entity(bool async) + { + await base.Custom_naming_projection_owner_entity(async); + + AssertSql( + """ +SELECT j."Id", j."Title", j.json_collection_custom_naming, j.json_reference_custom_naming +FROM "JsonEntitiesCustomNaming" AS j +"""); + } + + public override async Task Custom_naming_projection_owned_reference(bool async) + { + await base.Custom_naming_projection_owned_reference(async); + + AssertSql( + """ +SELECT j.json_reference_custom_naming -> 'Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸', j."Id" +FROM "JsonEntitiesCustomNaming" AS j +"""); + } + + public override async Task Custom_naming_projection_owned_collection(bool async) + { + await base.Custom_naming_projection_owned_collection(async); + + AssertSql( + """ +SELECT j.json_collection_custom_naming, j."Id" +FROM "JsonEntitiesCustomNaming" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Custom_naming_projection_owned_scalar(bool async) + { + await base.Custom_naming_projection_owned_scalar(async); + + AssertSql( + """ +SELECT CAST(j.json_reference_custom_naming #>> ARRAY['Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸','ユニコーンFraction一角獣']::text[] AS double precision) +FROM "JsonEntitiesCustomNaming" AS j +"""); + } + + public override async Task Custom_naming_projection_everything(bool async) + { + await base.Custom_naming_projection_everything(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 -> '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 +"""); + } + + public override async Task Project_entity_with_single_owned(bool async) + { + await base.Project_entity_with_single_owned(async); + + AssertSql( + """ +SELECT j."Id", j."Name", j."OwnedCollection" +FROM "JsonEntitiesSingleOwned" AS j +"""); + } + + public override async Task LeftJoin_json_entities(bool async) + { + await base.LeftJoin_json_entities(async); + + AssertSql( + """ +SELECT j."Id", j."Name", j."OwnedCollection", j0."Id", j0."EntityBasicId", j0."Name", j0."OwnedCollectionRoot", j0."OwnedReferenceRoot" +FROM "JsonEntitiesSingleOwned" AS j +LEFT JOIN "JsonEntitiesBasic" AS j0 ON j."Id" = j0."Id" +"""); + } + + public override async Task RightJoin_json_entities(bool async) + { + await base.RightJoin_json_entities(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."OwnedCollection" +FROM "JsonEntitiesBasic" AS j +RIGHT JOIN "JsonEntitiesSingleOwned" AS j0 ON j."Id" = j0."Id" +"""); + } + + public override async Task Left_join_json_entities_complex_projection(bool async) + { + await base.Left_join_json_entities_complex_projection(async); + + AssertSql( + """ +SELECT j."Id", j0."Id", j0."OwnedReferenceRoot", j0."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j0."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j0."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}' +FROM "JsonEntitiesSingleOwned" AS j +LEFT JOIN "JsonEntitiesBasic" AS j0 ON j."Id" = j0."Id" +"""); + } + + public override async Task Left_join_json_entities_json_being_inner(bool async) + { + await base.Left_join_json_entities_json_being_inner(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."OwnedCollection" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesSingleOwned" AS j0 ON j."Id" = j0."Id" +"""); + } + + public override async Task Left_join_json_entities_complex_projection_json_being_inner(bool async) + { + await base.Left_join_json_entities_complex_projection_json_being_inner(async); + + AssertSql( + """ +SELECT j."Id", j0."Id", j."OwnedReferenceRoot", j."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j0."Name" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesSingleOwned" AS j0 ON j."Id" = j0."Id" +"""); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(async); + + AssertSql( + """ +SELECT ( + SELECT CAST(j0."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Date}' AS timestamp without time zone) + FROM "JsonEntitiesBasic" AS j0 + ORDER BY j0."Id" NULLS FIRST + LIMIT 1) +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(bool async) + { + await base.Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(async); + + AssertSql( + @""); + } + + public override async Task Json_entity_with_inheritance_basic_projection(bool async) + { + await base.Json_entity_with_inheritance_basic_projection(async); + + AssertSql( + """ +SELECT j."Id", j."Discriminator", j."Name", j."Fraction", j."CollectionOnBase", j."ReferenceOnBase", j."CollectionOnDerived", j."ReferenceOnDerived" +FROM "JsonEntitiesInheritance" AS j +"""); + } + + public override async Task Json_entity_with_inheritance_project_derived(bool async) + { + await base.Json_entity_with_inheritance_project_derived(async); + + AssertSql( + """ +SELECT j."Id", j."Discriminator", j."Name", j."Fraction", j."CollectionOnBase", j."ReferenceOnBase", j."CollectionOnDerived", j."ReferenceOnDerived" +FROM "JsonEntitiesInheritance" AS j +WHERE j."Discriminator" = 'JsonEntityInheritanceDerived' +"""); + } + + public override async Task Json_entity_with_inheritance_project_navigations(bool async) + { + await base.Json_entity_with_inheritance_project_navigations(async); + + AssertSql( + """ +SELECT j."Id", j."ReferenceOnBase", j."CollectionOnBase" +FROM "JsonEntitiesInheritance" AS j +"""); + } + + public override async Task Json_entity_with_inheritance_project_navigations_on_derived(bool async) + { + await base.Json_entity_with_inheritance_project_navigations_on_derived(async); + + AssertSql( + """ +SELECT j."Id", j."ReferenceOnBase", j."ReferenceOnDerived", j."CollectionOnBase", j."CollectionOnDerived" +FROM "JsonEntitiesInheritance" AS j +WHERE j."Discriminator" = 'JsonEntityInheritanceDerived' +"""); + } + + public override async Task Json_entity_backtracking(bool async) + { + await base.Json_entity_backtracking(async); + + AssertSql( + @""); + } + + public override async Task Json_collection_index_in_projection_basic(bool async) + { + await base.Json_collection_index_in_projection_basic(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" -> 1, j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_ElementAt_in_projection(bool async) + { + await base.Json_collection_ElementAt_in_projection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" -> 1, j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_ElementAtOrDefault_in_projection(bool async) + { + await base.Json_collection_ElementAtOrDefault_in_projection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" -> 1, j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_project_collection(bool async) + { + await base.Json_collection_index_in_projection_project_collection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_ElementAt_project_collection(bool async) + { + await base.Json_collection_ElementAt_project_collection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_ElementAtOrDefault_project_collection(bool async) + { + await base.Json_collection_ElementAtOrDefault_project_collection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_parameter(bool async) + { + await base.Json_collection_index_in_projection_using_parameter(async); + + AssertSql( + """ +@prm='0' + +SELECT j."OwnedCollectionRoot" -> @prm, j."Id", @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_column(bool async) + { + await base.Json_collection_index_in_projection_using_column(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" -> j."Id", j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_untranslatable_client_method(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Json_collection_index_in_projection_using_untranslatable_client_method(async))).Message; + + Assert.Contains( + CoreStrings.QueryUnableToTranslateMethod( + "Microsoft.EntityFrameworkCore.Query.JsonQueryTestBase", + "MyMethod"), + message); + } + + public override async Task Json_collection_index_in_projection_using_untranslatable_client_method2(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Json_collection_index_in_projection_using_untranslatable_client_method2(async))).Message; + + Assert.Contains( + CoreStrings.QueryUnableToTranslateMethod( + "Microsoft.EntityFrameworkCore.Query.JsonQueryTestBase", + "MyMethod"), + message); + } + + public override async Task Json_collection_index_outside_bounds(bool async) + { + await base.Json_collection_index_outside_bounds(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" -> 25, j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_outside_bounds2(bool async) + { + await base.Json_collection_index_outside_bounds2(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,25}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_outside_bounds_with_property_access(bool async) + { + await base.Json_collection_index_outside_bounds_with_property_access(async); + + AssertSql( + """ +SELECT CAST(j."OwnedCollectionRoot" #>> '{25,Number}' AS integer) +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_index_in_projection_nested(bool async) + { + await base.Json_collection_index_in_projection_nested(async); + + AssertSql( + """ +@prm='1' + +SELECT j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm]::text[], j."Id", @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_nested_project_scalar(bool async) + { + await base.Json_collection_index_in_projection_nested_project_scalar(async); + + AssertSql( + """ +@prm='1' + +SELECT CAST(j."OwnedCollectionRoot" #>> ARRAY[0,'OwnedCollectionBranch',@prm,'Date']::text[] AS timestamp without time zone) +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_nested_project_reference(bool async) + { + await base.Json_collection_index_in_projection_nested_project_reference(async); + + AssertSql( + """ +@prm='1' + +SELECT j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm,'OwnedReferenceLeaf']::text[], j."Id", @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_nested_project_collection(bool async) + { + await base.Json_collection_index_in_projection_nested_project_collection(async); + + AssertSql( + """ +@prm='1' + +SELECT j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm,'OwnedCollectionLeaf']::text[], j."Id", @prm +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_index_in_projection_nested_project_collection_anonymous_projection(bool async) + { + await base.Json_collection_index_in_projection_nested_project_collection_anonymous_projection(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm,'OwnedCollectionLeaf']::text[], @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_predicate_using_constant(bool async) + { + await base.Json_collection_index_in_predicate_using_constant(async); + + AssertSql( + """ +SELECT j."Id" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> '{0,Name}') <> 'Foo' OR (j."OwnedCollectionRoot" #>> '{0,Name}') IS NULL +"""); + } + + public override async Task Json_collection_index_in_predicate_using_variable(bool async) + { + await base.Json_collection_index_in_predicate_using_variable(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> ARRAY[@prm,'Name']::text[]) <> 'Foo' OR (j."OwnedCollectionRoot" #>> ARRAY[@prm,'Name']::text[]) IS NULL +"""); + } + + public override async Task Json_collection_index_in_predicate_using_column(bool async) + { + await base.Json_collection_index_in_predicate_using_column(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> ARRAY[j."Id",'Name']::text[]) = 'e1_c2' +"""); + } + + public override async Task Json_collection_index_in_predicate_using_complex_expression1(bool async) + { + await base.Json_collection_index_in_predicate_using_complex_expression1(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> ARRAY[CASE + WHEN j."Id" = 1 THEN 0 + ELSE 1 +END,'Name']::text[]) = 'e1_c1' +"""); + } + + public override async Task Json_collection_index_in_predicate_using_complex_expression2(bool async) + { + await base.Json_collection_index_in_predicate_using_complex_expression2(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> ARRAY[( + SELECT max(j0."Id") + FROM "JsonEntitiesBasic" AS j0),'Name']::text[]) = 'e1_c2' +"""); + } + + public override async Task Json_collection_ElementAt_in_predicate(bool async) + { + await base.Json_collection_ElementAt_in_predicate(async); + + AssertSql( + """ +SELECT j."Id" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> '{1,Name}') <> 'Foo' OR (j."OwnedCollectionRoot" #>> '{1,Name}') IS NULL +"""); + } + + public override async Task Json_collection_index_in_predicate_nested_mix(bool async) + { + await base.Json_collection_index_in_predicate_nested_mix(async); + + AssertSql( + """ +@prm='0' + +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE (j."OwnedCollectionRoot" #>> ARRAY[1,'OwnedCollectionBranch',@prm,'OwnedCollectionLeaf',j."Id" - 1,'SomethingSomething']::text[]) = 'e1_c2_c1_c1' +"""); + } + + public override async Task Json_collection_ElementAt_and_pushdown(bool async) + { + await base.Json_collection_ElementAt_and_pushdown(async); + + AssertSql( + """ +SELECT j."Id", CAST(j."OwnedCollectionRoot" #>> '{0,Number}' AS integer) AS "CollectionElement" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_Any_with_predicate(bool async) + { + await base.Json_collection_Any_with_predicate(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE EXISTS ( + SELECT 1 + FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ("OwnedReferenceLeaf" jsonb)) WITH ORDINALITY AS o + WHERE (o."OwnedReferenceLeaf" ->> 'SomethingSomething') = 'e1_r_c1_r') +"""); + } + + public override async Task Json_collection_Where_ElementAt(bool async) + { + await base.Json_collection_Where_ElementAt(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE ( + SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' + FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( + "Date" timestamp without time zone, + "Enum" integer, + "Fraction" numeric(18,2), + "Id" integer, + "OwnedReferenceLeaf" jsonb + )) WITH ORDINALITY AS o + WHERE o."Enum" = -3 + LIMIT 1 OFFSET 0) = 'e1_r_c2_r' +"""); + } + + public override async Task Json_collection_Skip(bool async) + { + await base.Json_collection_Skip(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE ( + SELECT o0.c + FROM ( + 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 o0 + LIMIT 1 OFFSET 0) = 'e1_r_c2_r' +"""); + } + + public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool async) + { + await base.Json_collection_OrderByDescending_Skip_ElementAt(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE ( + SELECT o0.c + FROM ( + 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, + "Fraction" numeric(18,2), + "Id" integer, + "OwnedReferenceLeaf" jsonb + )) WITH ORDINALITY AS o + ORDER BY o."Date" DESC NULLS LAST + OFFSET 1 + ) AS o0 + ORDER BY o0.c0 DESC NULLS LAST + LIMIT 1 OFFSET 0) = 'e1_r_c1_r' +"""); + } + + public override async Task Json_collection_Distinct_Count_with_predicate(bool async) + { + await base.Json_collection_Distinct_Count_with_predicate(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE ( + SELECT count(*)::int + FROM ( + 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 o0) = 1 +"""); + } + + public override async Task Json_collection_within_collection_Count(bool async) + { + await base.Json_collection_within_collection_Count(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE EXISTS ( + SELECT 1 + 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 ( + "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) = 2) +"""); + } + + public override async Task Json_collection_in_projection_with_composition_count(bool async) + { + await base.Json_collection_in_projection_with_composition_count(async); + + AssertSql( + """ +SELECT ( + SELECT count(*)::int + 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) +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_in_projection_with_anonymous_projection_of_scalars(bool async) + { + await base.Json_collection_in_projection_with_anonymous_projection_of_scalars(async); + + AssertSql( + """ +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, + "Number" integer +)) WITH ORDINALITY AS o ON TRUE +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_scalars(bool async) + { + await base.Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_scalars(async); + + AssertSql( + """ +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, + "Number" integer + )) WITH ORDINALITY AS o + WHERE o."Name" = 'Foo' +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_primitive_arrays(bool async) + { + await base.Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_primitive_arrays(async); + + AssertSql( + """ +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[] + )) WITH ORDINALITY AS o + WHERE o."Name" = 'Foo' +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_filter_in_projection(bool async) + { + await base.Json_collection_filter_in_projection(async); + + AssertSql( + """ +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."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, + "Numbers" integer[], + "OwnedCollectionBranch" jsonb, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o + WHERE o."Name" <> 'Foo' OR o."Name" IS NULL +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_nested_collection_filter_in_projection(bool async) + { + await base.Json_nested_collection_filter_in_projection(async); + + AssertSql( + """ +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, 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."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 o1 ON TRUE +) AS s ON TRUE +ORDER BY j."Id" NULLS FIRST, s.ordinality NULLS FIRST +"""); + } + + public override async Task Json_nested_collection_anonymous_projection_in_projection(bool async) + { + await base.Json_nested_collection_anonymous_projection_in_projection(async); + + AssertSql( + """ +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 ("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, + "OwnedCollectionLeaf" jsonb, + "OwnedReferenceLeaf" jsonb + )) 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_collection_skip_take_in_projection(bool async) + { + await base.Json_collection_skip_take_in_projection(async); + + AssertSql( + """ +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."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, + "Numbers" integer[], + "OwnedCollectionBranch" jsonb, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o + ORDER BY o."Name" NULLS FIRST + LIMIT 5 OFFSET 1 +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0.c1 NULLS FIRST +"""); + } + + public override async Task Json_collection_skip_take_in_projection_project_into_anonymous_type(bool async) + { + await base.Json_collection_skip_take_in_projection_project_into_anonymous_type(async); + + AssertSql( + """ +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, + "Numbers" integer[], + "OwnedCollectionBranch" jsonb, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o + ORDER BY o."Name" NULLS FIRST + LIMIT 5 OFFSET 1 +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0.c NULLS FIRST +"""); + } + + public override async Task Json_collection_skip_take_in_projection_with_json_reference_access_as_final_operation(bool async) + { + await base.Json_collection_skip_take_in_projection_with_json_reference_access_as_final_operation(async); + + AssertSql( + """ +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, + "Number" integer, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o + ORDER BY o."Name" NULLS FIRST + LIMIT 5 OFFSET 1 +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST, o0.c0 NULLS FIRST +"""); + } + + public override async Task Json_collection_distinct_in_projection(bool async) + { + await base.Json_collection_distinct_in_projection(async); + + AssertSql( + """ +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."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, + "Numbers" integer[], + "OwnedCollectionBranch" jsonb, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o +) 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 +"""); + } + + public override async Task Json_collection_anonymous_projection_distinct_in_projection(bool async) + { + await base.Json_collection_anonymous_projection_distinct_in_projection(async); + + AssertSql(""); + } + + public override async Task Json_collection_leaf_filter_in_projection(bool async) + { + await base.Json_collection_leaf_filter_in_projection(async); + + AssertSql( + """ +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 o0 ON TRUE +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_multiple_collection_projections(bool async) + { + await base.Json_multiple_collection_projections(async); + + AssertSql( + """ +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 o4 ON TRUE +LEFT JOIN LATERAL ( + 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, + "Numbers" integer[], + "OwnedCollectionBranch" jsonb, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o0 +) AS o1 ON TRUE +LEFT JOIN LATERAL ( + 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", 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 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, 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 +"""); + } + + public override async Task Json_branch_collection_distinct_and_other_collection(bool async) + { + await base.Json_branch_collection_distinct_and_other_collection(async); + + AssertSql( + """ +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."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 o0 ON TRUE +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +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 +"""); + } + + public override async Task Json_leaf_collection_distinct_and_other_collection(bool async) + { + await base.Json_leaf_collection_distinct_and_other_collection(async); + + AssertSql( + """ +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 o0 ON TRUE +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST, o0."SomethingSomething" NULLS FIRST +"""); + } + + public override async Task Json_collection_of_primitives_SelectMany(bool async) + { + await base.Json_collection_of_primitives_SelectMany(async); + + 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) + { + await base.Json_collection_of_primitives_index_used_in_predicate(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE ((ARRAY(SELECT CAST(element AS text) FROM jsonb_array_elements_text(j."OwnedReferenceRoot" -> 'Names') WITH ORDINALITY AS t(element) ORDER BY ordinality)))[1] = 'e1_r1' +"""); + } + + public override async Task Json_collection_of_primitives_index_used_in_projection(bool async) + { + await base.Json_collection_of_primitives_index_used_in_projection(async); + + AssertSql( + """ +SELECT ((ARRAY(SELECT CAST(element AS integer) FROM jsonb_array_elements_text(j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,Enums}') WITH ORDINALITY AS t(element) ORDER BY ordinality)))[1] +FROM "JsonEntitiesBasic" AS j +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_of_primitives_index_used_in_orderby(bool async) + { + await base.Json_collection_of_primitives_index_used_in_orderby(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +ORDER BY ((ARRAY(SELECT CAST(element AS integer) FROM jsonb_array_elements_text(j."OwnedReferenceRoot" -> 'Numbers') WITH ORDINALITY AS t(element) ORDER BY ordinality)))[1] NULLS FIRST +"""); + } + + public override async Task Json_collection_of_primitives_contains_in_predicate(bool async) + { + await base.Json_collection_of_primitives_contains_in_predicate(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +WHERE 'e1_r1' = ANY ((ARRAY(SELECT CAST(element AS text) FROM jsonb_array_elements_text(j."OwnedReferenceRoot" -> 'Names') WITH ORDINALITY AS t(element) ORDER BY ordinality))) +"""); + } + + public override async Task Json_collection_index_with_parameter_Select_ElementAt(bool async) + { + await base.Json_collection_index_with_parameter_Select_ElementAt(async); + + AssertSql( + """ +@prm='0' + +SELECT j."Id", ( + SELECT 'Foo' + FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch']::text[]) 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 + LIMIT 1 OFFSET 0) AS "CollectionElement" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_with_expression_Select_ElementAt(bool async) + { + await base.Json_collection_index_with_expression_Select_ElementAt(async); + + AssertSql( + """ +@prm='0' + +SELECT j."OwnedCollectionRoot" #>> ARRAY[@prm + j."Id",'OwnedCollectionBranch',0,'OwnedReferenceLeaf','SomethingSomething']::text[] +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_Select_entity_collection_ElementAt(bool async) + { + await base.Json_collection_Select_entity_collection_ElementAt(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" #> '{0,OwnedCollectionBranch}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_Select_entity_ElementAt(bool async) + { + await base.Json_collection_Select_entity_ElementAt(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j."Id" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_Select_entity_in_anonymous_object_ElementAt(bool async) + { + await base.Json_collection_Select_entity_in_anonymous_object_ElementAt(async); + + AssertSql( + """ +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 ("OwnedReferenceBranch" jsonb)) WITH ORDINALITY AS o + LIMIT 1 OFFSET 0 +) AS o0 ON TRUE +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async) + { + await base.Json_collection_Select_entity_with_initializer_ElementAt(async); + + AssertSql( + """ +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, + "Numbers" integer[], + "OwnedCollectionBranch" jsonb, + "OwnedReferenceBranch" jsonb + )) WITH ORDINALITY AS o + LIMIT 1 OFFSET 0 +) AS o0 ON TRUE +"""); + } + + public override async Task Json_projection_deduplication_with_collection_indexer_in_original(bool async) + { + await base.Json_projection_deduplication_with_collection_indexer_in_original(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j."OwnedCollectionRoot" -> 0, j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch,OwnedCollectionLeaf}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_deduplication_with_collection_indexer_in_target(bool async) + { + await base.Json_projection_deduplication_with_collection_indexer_in_target(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,1}', j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> ARRAY['OwnedReferenceBranch','OwnedCollectionLeaf',@prm]::text[], @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(bool async) + { + await base.Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(async); + + AssertSql( + """ +@prm='1' + +SELECT j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',0,'OwnedCollectionLeaf',@prm]::text[], j."Id", @prm, j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@prm]::text[], j."OwnedReferenceRoot" -> 'OwnedCollectionBranch', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async) + { + await base.Json_collection_index_in_projection_using_constant_when_owner_is_present(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" -> 1 +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_constant_when_owner_is_not_present(bool async) + { + await base.Json_collection_index_in_projection_using_constant_when_owner_is_not_present(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedCollectionRoot" -> 1 +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_parameter_when_owner_is_present(bool async) + { + await base.Json_collection_index_in_projection_using_parameter_when_owner_is_present(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" -> @prm, @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_using_parameter_when_owner_is_not_present(bool async) + { + await base.Json_collection_index_in_projection_using_parameter_when_owner_is_not_present(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."OwnedCollectionRoot" -> @prm, @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(bool async) + { + await base.Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_not_present(bool async) + { + await base.Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_not_present(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(bool async) + { + await base.Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch']::text[], @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_not_present(bool async) + { + await base.Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_not_present(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch']::text[], @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_when_owner_is_present_misc1(bool async) + { + await base.Json_collection_index_in_projection_when_owner_is_present_misc1(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> ARRAY[1,'OwnedCollectionBranch',@prm]::text[], @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_when_owner_is_not_present_misc1(bool async) + { + await base.Json_collection_index_in_projection_when_owner_is_not_present_misc1(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[1,'OwnedCollectionBranch',@prm]::text[], @prm +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_when_owner_is_present_misc2(bool async) + { + await base.Json_collection_index_in_projection_when_owner_is_present_misc2(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,1}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_when_owner_is_not_present_misc2(bool async) + { + await base.Json_collection_index_in_projection_when_owner_is_not_present_misc2(async); + + AssertSql( + """ +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,1}' +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_when_owner_is_present_multiple(bool async) + { + await base.Json_collection_index_in_projection_when_owner_is_present_multiple(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',1]::text[], @prm, j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch,1,OwnedReferenceLeaf}', j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',j."Id"]::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',1,'OwnedReferenceLeaf']::text[], j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',j."Id"]::text[] +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_collection_index_in_projection_when_owner_is_not_present_multiple(bool async) + { + await base.Json_collection_index_in_projection_when_owner_is_not_present_multiple(async); + + AssertSql( + """ +@prm='1' + +SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',1]::text[], @prm, j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch,1,OwnedReferenceLeaf}', j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',j."Id"]::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',1,'OwnedReferenceLeaf']::text[], j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',j."Id"]::text[] +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_scalar_required_null_semantics(bool async) + { + await base.Json_scalar_required_null_semantics(async); + + AssertSql( + """ +SELECT j."Name" +FROM "JsonEntitiesBasic" AS j +WHERE (CAST(j."OwnedReferenceRoot" ->> 'Number' AS integer)) <> length(j."OwnedReferenceRoot" ->> 'Name')::int OR (j."OwnedReferenceRoot" ->> 'Name') IS NULL +"""); + } + + public override async Task Json_scalar_optional_null_semantics(bool async) + { + await base.Json_scalar_optional_null_semantics(async); + + AssertSql( + """ +SELECT j."Name" +FROM "JsonEntitiesBasic" AS j +WHERE ((j."OwnedReferenceRoot" ->> 'Name') <> (j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}') OR (j."OwnedReferenceRoot" ->> 'Name') IS NULL OR (j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}') IS NULL) AND ((j."OwnedReferenceRoot" ->> 'Name') IS NOT NULL OR (j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}') IS NOT NULL) +"""); + } + + public override async Task Group_by_on_json_scalar(bool async) + { + await base.Group_by_on_json_scalar(async); + + AssertSql( + """ +SELECT j0."Key", count(*)::int AS "Count" +FROM ( + SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j +) AS j0 +GROUP BY j0."Key" +"""); + } + + public override async Task Group_by_on_json_scalar_using_collection_indexer(bool async) + { + await base.Group_by_on_json_scalar_using_collection_indexer(async); + + AssertSql( + """ +SELECT j0."Key", count(*)::int AS "Count" +FROM ( + SELECT j."OwnedCollectionRoot" #>> '{0,Name}' AS "Key" + FROM "JsonEntitiesBasic" AS j +) AS j0 +GROUP BY j0."Key" +"""); + } + + public override async Task Group_by_First_on_json_scalar(bool async) + { + await base.Group_by_First_on_json_scalar(async); + + AssertSql( + """ +SELECT j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 +FROM ( + SELECT j0."Key" + FROM ( + SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j + ) AS j0 + GROUP BY j0."Key" +) AS j3 +LEFT JOIN ( + SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c AS c, j4.c0 AS c0, j4."Key" + FROM ( + 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 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" +"""); + } + + public override async Task Group_by_FirstOrDefault_on_json_scalar(bool async) + { + await base.Group_by_FirstOrDefault_on_json_scalar(async); + + AssertSql( + """ +SELECT j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 +FROM ( + SELECT j0."Key" + FROM ( + SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j + ) AS j0 + GROUP BY j0."Key" +) AS j3 +LEFT JOIN ( + SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c AS c, j4.c0 AS c0, j4."Key" + FROM ( + 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 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" +"""); + } + + public override async Task Group_by_Skip_Take_on_json_scalar(bool async) + { + await base.Group_by_Skip_Take_on_json_scalar(async); + + AssertSql( + """ +SELECT j3."Key", j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 +FROM ( + SELECT j0."Key" + FROM ( + SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" + FROM "JsonEntitiesBasic" AS j + ) AS j0 + GROUP BY j0."Key" +) AS j3 +LEFT JOIN ( + SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c, j4.c0, j4."Key" + FROM ( + 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 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 +"""); + } + + public override async Task Group_by_json_scalar_Orderby_json_scalar_FirstOrDefault(bool async) + { + await base.Group_by_json_scalar_Orderby_json_scalar_FirstOrDefault(async); + + AssertSql( + @""); + } + + public override async Task Group_by_json_scalar_Skip_First_project_json_scalar(bool async) + { + await base.Group_by_json_scalar_Skip_First_project_json_scalar(async); + + AssertSql( + """ +SELECT ( + SELECT CAST(j1.c0 #>> '{OwnedReferenceBranch,Enum}' AS integer) + FROM ( + 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 j0 +GROUP BY j0."Key" +"""); + } + + public override async Task Json_with_include_on_json_entity(bool async) + { + await base.Json_with_include_on_json_entity(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Json_with_include_on_entity_reference(bool async) + { + await base.Json_with_include_on_entity_reference(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" +"""); + } + + public override async Task Json_with_include_on_entity_collection(bool async) + { + await base.Json_with_include_on_entity_collection(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Entity_including_collection_with_json(bool async) + { + await base.Entity_including_collection_with_json(async); + + AssertSql( + """ +SELECT e."Id", e."Name", j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "EntitiesBasic" AS e +LEFT JOIN "JsonEntitiesBasic" AS j ON e."Id" = j."EntityBasicId" +ORDER BY e."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_include_on_entity_collection_and_reference(bool async) + { + await base.Json_with_include_on_entity_collection_and_reference(async); + + AssertSql( + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."ParentId", j1."Id", j1."Name", j1."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" +LEFT JOIN "JsonEntitiesBasicForCollection" AS j1 ON j."Id" = j1."ParentId" +ORDER BY j."Id" NULLS FIRST, j0."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_json_reference_leaf_and_entity_collection(bool async) + { + await base.Json_with_projection_of_json_reference_leaf_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."Id", j0."Id", j0."Name", j0."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_json_reference_and_entity_collection(bool async) + { + await base.Json_with_projection_of_json_reference_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot", j."Id", j0."Id", j0."Name", j0."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_multiple_json_references_and_entity_collection(bool async) + { + await base.Json_with_projection_of_multiple_json_references_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot", j."Id", j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j0."Id", j0."Name", j0."ParentId", j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch,OwnedReferenceLeaf}', j."OwnedCollectionRoot" #> '{0,OwnedCollectionBranch,0,OwnedReferenceLeaf}' +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_json_collection_leaf_and_entity_collection(bool async) + { + await base.Json_with_projection_of_json_collection_leaf_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."Id", j0."Id", j0."Name", j0."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_json_collection_and_entity_collection(bool async) + { + await base.Json_with_projection_of_json_collection_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot", j."Id", j0."Id", j0."Name", j0."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" +ORDER BY j."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_json_collection_element_and_entity_collection(bool async) + { + await base.Json_with_projection_of_json_collection_element_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedCollectionRoot" -> 0, j."Id", j0."Id", j0."Name", j0."ParentId", j1."Id", j1."Name", j1."ParentId" +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" +LEFT JOIN "JsonEntitiesBasicForCollection" AS j1 ON j."Id" = j1."ParentId" +ORDER BY j."Id" NULLS FIRST, j0."Id" NULLS FIRST +"""); + } + + public override async Task Json_with_projection_of_mix_of_json_collections_json_references_and_entity_collection(bool async) + { + await base.Json_with_projection_of_mix_of_json_collections_json_references_and_entity_collection(async); + + AssertSql( + """ +SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."Id", j0."Id", j0."Name", j0."ParentId", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j1."Id", j1."Name", j1."ParentId", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,0}', j."OwnedReferenceRoot" -> 'OwnedCollectionBranch', j."OwnedCollectionRoot", j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> '{0,OwnedCollectionBranch}' +FROM "JsonEntitiesBasic" AS j +LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" +LEFT JOIN "JsonEntitiesBasicForCollection" AS j1 ON j."Id" = j1."ParentId" +ORDER BY j."Id" NULLS FIRST, j0."Id" NULLS FIRST +"""); + +// AssertSql( +//""" +//SELECT JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedCollectionLeaf'), [j].[Id], [j0].[Id], [j0].[Name], [j0].[ParentId], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedReferenceLeaf'), [j1].[Id], [j1].[Name], [j1].[ParentId], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch'), [j].[OwnedCollectionRoot] +//FROM [JsonEntitiesBasic] AS [j] +//LEFT JOIN [JsonEntitiesBasicForReference] AS [j0] ON [j].[Id] = [j0].[ParentId] +//LEFT JOIN [JsonEntitiesBasicForCollection] AS [j1] ON [j].[Id] = [j1].[ParentId] +//ORDER BY [j].[Id], [j0].[Id] +//"""); + } + + public override async Task Json_all_types_entity_projection(bool async) + { + await base.Json_all_types_entity_projection(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 +"""); + } + + public override async Task Json_all_types_projection_from_owned_entity_reference(bool async) + { + await base.Json_all_types_projection_from_owned_entity_reference(async); + + AssertSql( + """ +SELECT j."Reference", j."Id" +FROM "JsonEntitiesAllTypes" AS j +"""); + } + + public override async Task Json_all_types_projection_individual_properties(bool async) + { + await base.Json_all_types_projection_individual_properties(async); + + AssertSql( + """ +SELECT j."Reference" ->> 'TestDefaultString' AS "TestDefaultString", j."Reference" ->> 'TestMaxLengthString' AS "TestMaxLengthString", CAST(j."Reference" ->> 'TestBoolean' AS boolean) AS "TestBoolean", CAST(j."Reference" ->> 'TestByte' AS smallint) AS "TestByte", CAST(j."Reference" ->> 'TestCharacter' AS character(1)) AS "TestCharacter", CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone) AS "TestDateTime", CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone) AS "TestDateTimeOffset", CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3)) AS "TestDecimal", CAST(j."Reference" ->> 'TestDouble' AS double precision) AS "TestDouble", CAST(j."Reference" ->> 'TestGuid' AS uuid) AS "TestGuid", CAST(j."Reference" ->> 'TestInt16' AS smallint) AS "TestInt16", CAST(j."Reference" ->> 'TestInt32' AS integer) AS "TestInt32", CAST(j."Reference" ->> 'TestInt64' AS bigint) AS "TestInt64", CAST(j."Reference" ->> 'TestSignedByte' AS smallint) AS "TestSignedByte", CAST(j."Reference" ->> 'TestSingle' AS real) AS "TestSingle", CAST(j."Reference" ->> 'TestTimeSpan' AS interval) AS "TestTimeSpan", CAST(j."Reference" ->> 'TestDateOnly' AS date) AS "TestDateOnly", CAST(j."Reference" ->> 'TestTimeOnly' AS time without time zone) AS "TestTimeOnly", CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer) AS "TestUnsignedInt16", CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint) AS "TestUnsignedInt32", CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0)) AS "TestUnsignedInt64", CAST(j."Reference" ->> 'TestEnum' AS integer) AS "TestEnum", CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer) AS "TestEnumWithIntConverter", CAST(j."Reference" ->> 'TestNullableEnum' AS integer) AS "TestNullableEnum", CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer) AS "TestNullableEnumWithIntConverter", j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls' AS "TestNullableEnumWithConverterThatHandlesNulls" +FROM "JsonEntitiesAllTypes" AS j +"""); + } + + public override async Task Json_boolean_predicate(bool async) + { + await base.Json_boolean_predicate(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 CAST(j."Reference" ->> 'TestBoolean' AS boolean) +"""); + } + + public override async Task Json_boolean_predicate_negated(bool async) + { + await base.Json_boolean_predicate_negated(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 NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) +"""); + } + + public override async Task Json_boolean_projection(bool async) + { + await base.Json_boolean_projection(async); + + AssertSql( + """ +SELECT CAST(j."Reference" ->> 'TestBoolean' AS boolean) +FROM "JsonEntitiesAllTypes" AS j +"""); + } + + public override async Task Json_boolean_projection_negated(bool async) + { + await base.Json_boolean_projection_negated(async); + + AssertSql( + """ +SELECT NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) +FROM "JsonEntitiesAllTypes" AS j +"""); + } + + public override async Task Json_predicate_on_default_string(bool async) + { + await base.Json_predicate_on_default_string(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 (j."Reference" ->> 'TestDefaultString') <> 'MyDefaultStringInReference1' OR (j."Reference" ->> 'TestDefaultString') IS NULL +"""); + } + + public override async Task Json_predicate_on_max_length_string(bool async) + { + await base.Json_predicate_on_max_length_string(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 (j."Reference" ->> 'TestMaxLengthString') <> 'Foo' OR (j."Reference" ->> 'TestMaxLengthString') IS NULL +"""); + } + + public override async Task Json_predicate_on_string_condition(bool async) + { + await base.Json_predicate_on_string_condition(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 CASE + WHEN NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) THEN j."Reference" ->> 'TestMaxLengthString' + ELSE j."Reference" ->> 'TestDefaultString' +END = 'MyDefaultStringInReference1' +"""); + } + + public override async Task Json_predicate_on_byte(bool async) + { + await base.Json_predicate_on_byte(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 (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."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 +"""); + } + + public override async Task Json_predicate_on_datetime(bool async) + { + await base.Json_predicate_on_datetime(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 (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 +"""); + } + + public override async Task Json_predicate_on_datetimeoffset(bool async) + { + await base.Json_predicate_on_datetimeoffset(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 (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 +"""); + } + + public override async Task Json_predicate_on_decimal(bool async) + { + await base.Json_predicate_on_decimal(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 (CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3))) <> 1.35 OR (CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3))) IS NULL +"""); + } + + public override async Task Json_predicate_on_double(bool async) + { + await base.Json_predicate_on_double(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 (CAST(j."Reference" ->> 'TestDouble' AS double precision)) <> 33.25 OR (CAST(j."Reference" ->> 'TestDouble' AS double precision)) IS NULL +"""); + } + + public override async Task Json_predicate_on_enum(bool async) + { + await base.Json_predicate_on_enum(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 (CAST(j."Reference" ->> 'TestEnum' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestEnum' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_enumwithintconverter(bool async) + { + await base.Json_predicate_on_enumwithintconverter(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 (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) <> -3 OR (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_guid(bool async) + { + await base.Json_predicate_on_guid(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 (CAST(j."Reference" ->> 'TestGuid' AS uuid)) <> '00000000-0000-0000-0000-000000000000' OR (CAST(j."Reference" ->> 'TestGuid' AS uuid)) IS NULL +"""); + } + + public override async Task Json_predicate_on_int16(bool async) + { + await base.Json_predicate_on_int16(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 (CAST(j."Reference" ->> 'TestInt16' AS smallint)) <> 3 OR (CAST(j."Reference" ->> 'TestInt16' AS smallint)) IS NULL +"""); + } + + public override async Task Json_predicate_on_int32(bool async) + { + await base.Json_predicate_on_int32(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 (CAST(j."Reference" ->> 'TestInt32' AS integer)) <> 33 OR (CAST(j."Reference" ->> 'TestInt32' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_int64(bool async) + { + await base.Json_predicate_on_int64(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 (CAST(j."Reference" ->> 'TestInt64' AS bigint)) <> 333 OR (CAST(j."Reference" ->> 'TestInt64' AS bigint)) IS NULL +"""); + } + + public override async Task Json_predicate_on_nullableenum1(bool async) + { + await base.Json_predicate_on_nullableenum1(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 (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) <> -1 OR (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_nullableenum2(bool async) + { + await base.Json_predicate_on_nullableenum2(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 (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NOT NULL +"""); + } + + public override async Task Json_predicate_on_nullableenumwithconverter1(bool async) + { + await base.Json_predicate_on_nullableenumwithconverter1(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 (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_nullableenumwithconverter2(bool async) + { + await base.Json_predicate_on_nullableenumwithconverter2(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 (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NOT NULL +"""); + } + + public override async Task Json_predicate_on_nullableenumwithconverterthathandlesnulls1(bool async) + { + await base.Json_predicate_on_nullableenumwithconverterthathandlesnulls1(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 (j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls') <> 'One' OR (j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls') IS NULL +"""); + } + + public override async Task Json_predicate_on_nullableenumwithconverterthathandlesnulls2(bool async) + { + await base.Json_predicate_on_nullableenumwithconverterthathandlesnulls2(async); + + AssertSql( + """ +x +"""); + } + + public override async Task Json_predicate_on_nullableint321(bool async) + { + await base.Json_predicate_on_nullableint321(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 (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) <> 100 OR (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_nullableint322(bool async) + { + await base.Json_predicate_on_nullableint322(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 (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) IS NOT NULL +"""); + } + + public override async Task Json_predicate_on_signedbyte(bool async) + { + await base.Json_predicate_on_signedbyte(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 (CAST(j."Reference" ->> 'TestSignedByte' AS smallint)) <> 100 OR (CAST(j."Reference" ->> 'TestSignedByte' AS smallint)) IS NULL +"""); + } + + public override async Task Json_predicate_on_single(bool async) + { + await base.Json_predicate_on_single(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 (CAST(j."Reference" ->> 'TestSingle' AS real)) <> 10.4 OR (CAST(j."Reference" ->> 'TestSingle' AS real)) IS NULL +"""); + } + + public override async Task Json_predicate_on_timespan(bool async) + { + await base.Json_predicate_on_timespan(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 (CAST(j."Reference" ->> 'TestTimeSpan' AS interval)) <> INTERVAL '03:02:00' OR (CAST(j."Reference" ->> 'TestTimeSpan' AS interval)) IS NULL +"""); + } + + public override async Task Json_predicate_on_dateonly(bool async) + { + await base.Json_predicate_on_dateonly(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 (CAST(j."Reference" ->> 'TestDateOnly' AS date)) <> DATE '0003-02-01' OR (CAST(j."Reference" ->> 'TestDateOnly' AS date)) IS NULL +"""); + } + + public override async Task Json_predicate_on_timeonly(bool async) + { + await base.Json_predicate_on_timeonly(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 (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 +"""); + } + + public override async Task Json_predicate_on_unisgnedint16(bool async) + { + await base.Json_predicate_on_unisgnedint16(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 (CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer)) <> 100 OR (CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer)) IS NULL +"""); + } + + public override async Task Json_predicate_on_unsignedint32(bool async) + { + await base.Json_predicate_on_unsignedint32(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 (CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint)) <> 1000 OR (CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint)) IS NULL +"""); + } + + public override async Task Json_predicate_on_unsignedint64(bool async) + { + await base.Json_predicate_on_unsignedint64(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 (CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0))) <> 10000.0 OR (CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0))) IS NULL +"""); + } + + public override async Task Json_predicate_on_bool_converted_to_int_zero_one(bool async) + { + await base.Json_predicate_on_bool_converted_to_int_zero_one(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (CAST(j."Reference" ->> 'BoolConvertedToIntZeroOne' AS integer)) = 1 +"""); + } + + public override async Task Json_predicate_on_bool_converted_to_int_zero_one_with_explicit_comparison(bool async) + { + await base.Json_predicate_on_bool_converted_to_int_zero_one_with_explicit_comparison(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (CAST(j."Reference" ->> 'BoolConvertedToIntZeroOne' AS integer)) = 0 +"""); + } + + public override async Task Json_predicate_on_bool_converted_to_string_True_False(bool async) + { + await base.Json_predicate_on_bool_converted_to_string_True_False(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (j."Reference" ->> 'BoolConvertedToStringTrueFalse') = 'True' +"""); + } + + public override async Task Json_predicate_on_bool_converted_to_string_True_False_with_explicit_comparison(bool async) + { + await base.Json_predicate_on_bool_converted_to_string_True_False_with_explicit_comparison(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (j."Reference" ->> 'BoolConvertedToStringTrueFalse') = 'True' +"""); + } + + public override async Task Json_predicate_on_bool_converted_to_string_Y_N(bool async) + { + await base.Json_predicate_on_bool_converted_to_string_Y_N(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (j."Reference" ->> 'BoolConvertedToStringYN') = 'Y' +"""); + } + + public override async Task Json_predicate_on_bool_converted_to_string_Y_N_with_explicit_comparison(bool async) + { + await base.Json_predicate_on_bool_converted_to_string_Y_N_with_explicit_comparison(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (j."Reference" ->> 'BoolConvertedToStringYN') = 'N' +"""); + } + + public override async Task Json_predicate_on_int_zero_one_converted_to_bool(bool async) + { + await base.Json_predicate_on_int_zero_one_converted_to_bool(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (CAST(j."Reference" ->> 'IntZeroOneConvertedToBool' AS boolean)) = TRUE +"""); + } + + public override async Task Json_predicate_on_string_True_False_converted_to_bool(bool async) + { + await base.Json_predicate_on_string_True_False_converted_to_bool(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (CAST(j."Reference" ->> 'StringTrueFalseConvertedToBool' AS boolean)) = FALSE +"""); + } + + public override async Task Json_predicate_on_string_Y_N_converted_to_bool(bool async) + { + await base.Json_predicate_on_string_Y_N_converted_to_bool(async); + + AssertSql( + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE (CAST(j."Reference" ->> 'StringYNConvertedToBool' AS boolean)) = FALSE +"""); + } + + public override async Task FromSql_on_entity_with_json_basic(bool async) + { + await base.FromSql_on_entity_with_json_basic(async); + + AssertSql( + """ +SELECT m."Id", m."EntityBasicId", m."Name", m."OwnedCollectionRoot", m."OwnedReferenceRoot" +FROM ( + SELECT * FROM "JsonEntitiesBasic" AS j +) AS m +"""); + } + + 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); + + AssertSql( + """ +SELECT m."OwnedReferenceRoot" -> 'OwnedReferenceBranch', m."Id" +FROM ( + SELECT * FROM "JsonEntitiesBasic" AS j +) AS m +"""); + } + + 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); + + AssertSql( + """ +SELECT m."OwnedReferenceRoot" -> 'OwnedCollectionBranch', m."Id" +FROM ( + SELECT * FROM "JsonEntitiesBasic" AS j +) AS m +"""); + } + + 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); + + AssertSql( + """ +SELECT m."Id", m."Discriminator", m."Name", m."Fraction", m."CollectionOnBase", m."ReferenceOnBase", m."CollectionOnDerived", m."ReferenceOnDerived" +FROM ( + SELECT * FROM "JsonEntitiesInheritance" AS j +) AS m +"""); + } + + 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); + + AssertSql( + """ +SELECT m."Id", m."Discriminator", m."Name", m."Fraction", m."CollectionOnBase", m."ReferenceOnBase", m."CollectionOnDerived", m."ReferenceOnDerived" +FROM ( + SELECT * FROM "JsonEntitiesInheritance" AS j +) AS m +WHERE m."Discriminator" = 'JsonEntityInheritanceDerived' +"""); + } + + 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); + + AssertSql( + """ +SELECT m."ReferenceOnBase", m."Id" +FROM ( + SELECT * FROM "JsonEntitiesInheritance" AS j +) AS m +ORDER BY m."Id" NULLS FIRST +"""); + } + + 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); + + AssertSql( + """ +SELECT m."CollectionOnDerived", m."Id" +FROM ( + SELECT * FROM "JsonEntitiesInheritance" AS j +) AS m +WHERE m."Discriminator" = 'JsonEntityInheritanceDerived' +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='1' + +SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',0,'OwnedCollectionLeaf',@prm]::text[], @prm +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' +@prm2='1' + +SELECT j."Id", j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@prm1,'OwnedCollectionLeaf',@prm2]::text[], @prm1, @prm2 +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' + +SELECT j."Id", j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@prm,'OwnedCollectionLeaf',1]::text[], @prm +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()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class JsonQueryGaussDBFixture : JsonQueryRelationalFixture, IQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + private JsonQueryData? _expectedData; + private readonly IReadOnlyDictionary _entityAsserters; + + public JsonQueryGaussDBFixture() + { + var entityAsserters = base.EntityAsserters.ToDictionary(); + + entityAsserters[typeof(JsonEntityAllTypes)] = (object e, object a) => + { + Assert.Equal(e is null, a is null); + + if (e is not null && a is not 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 is null, a is null); + + if (e is not null && a is not null) + { + var ee = (JsonOwnedAllTypes)e; + var aa = (JsonOwnedAllTypes)a; + + AssertAllTypes(ee, aa); + } + }; + + _entityAsserters = entityAsserters; + } + + IReadOnlyDictionary IQueryFixtureBase.EntityAsserters + => _entityAsserters; + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + base.ConfigureConventions(configurationBuilder); + + // The tests seed Unspecified DateTimes, but our default mapping for DateTime is timestamptz, which requires UTC. + // Map these properties to "timestamp without time zone". + configurationBuilder.Properties().HaveColumnType("timestamp without time zone"); + configurationBuilder.Properties>().HaveColumnType("timestamp without time zone[]"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // The following are ignored since we do not support mapping IList (as opposed to array/List) on regular properties + // (since that's not supported at the ADO.NET layer). However, we do support IList inside JSON documents, since that doesn't + // rely on ADO.NET support. + modelBuilder.Entity( + b => + { + b.Ignore(j => j.TestEnumCollection); + b.Ignore(j => j.TestUnsignedInt16Collection); + b.Ignore(j => j.TestNullableEnumCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollection); + b.Ignore(j => j.TestCharacterCollection); + b.Ignore(j => j.TestNullableInt32Collection); + b.Ignore(j => j.TestUnsignedInt64Collection); + + b.Ignore(j => j.TestByteCollection); + b.Ignore(j => j.TestBooleanCollection); + b.Ignore(j => j.TestDateTimeOffsetCollection); + b.Ignore(j => j.TestDoubleCollection); + b.Ignore(j => j.TestInt16Collection); + }); + + // These use collection types which are unsupported for arrays at the GaussDB 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 GaussDB (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() + { + if (_expectedData is null) + { + _expectedData = (JsonQueryData)base.GetExpectedData(); + + // The test data contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. + // Also chop sub-microsecond precision which GaussDB does not support. + foreach (var j in _expectedData.JsonEntitiesAllTypes) + { + j.Reference.TestDateTimeOffset = new DateTimeOffset( + j.Reference.TestDateTimeOffset.Ticks + - (j.Reference.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + + foreach (var j2 in j.Collection) + { + j2.TestDateTimeOffset = new DateTimeOffset( + j2.TestDateTimeOffset.Ticks - (j2.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), + TimeSpan.Zero); + } + + j.TestDateTimeOffsetCollection = j.TestDateTimeOffsetCollection.Select( + dto => new DateTimeOffset(dto.Ticks - (dto.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero)).ToList(); + } + } + + return _expectedData; + } + + 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 GaussDB does not support. + // See https://github.com/dotnet/efcore/issues/26068 + + var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic(); + var entitiesBasic = JsonQueryData.CreateEntitiesBasic(); + var jsonEntitiesBasicForReference = JsonQueryData.CreateJsonEntitiesBasicForReference(); + var jsonEntitiesBasicForCollection = JsonQueryData.CreateJsonEntitiesBasicForCollection(); + JsonQueryData.WireUp(jsonEntitiesBasic, entitiesBasic, jsonEntitiesBasicForReference, jsonEntitiesBasicForCollection); + + var jsonEntitiesCustomNaming = JsonQueryData.CreateJsonEntitiesCustomNaming(); + var jsonEntitiesSingleOwned = JsonQueryData.CreateJsonEntitiesSingleOwned(); + var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); + var jsonEntitiesAllTypes = JsonQueryData.CreateJsonEntitiesAllTypes(); + var jsonEntitiesConverters = JsonQueryData.CreateJsonEntitiesConverters(); + + context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic); + context.EntitiesBasic.AddRange(entitiesBasic); + context.JsonEntitiesBasicForReference.AddRange(jsonEntitiesBasicForReference); + context.JsonEntitiesBasicForCollection.AddRange(jsonEntitiesBasicForCollection); + context.JsonEntitiesCustomNaming.AddRange(jsonEntitiesCustomNaming); + context.JsonEntitiesSingleOwned.AddRange(jsonEntitiesSingleOwned); + context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); + context.JsonEntitiesAllTypes.AddRange(jsonEntitiesAllTypes); + context.JsonEntitiesConverters.AddRange(jsonEntitiesConverters); + + foreach (var j in jsonEntitiesAllTypes) + { + j.Reference.TestDateTimeOffset = new DateTimeOffset( + j.Reference.TestDateTimeOffset.Ticks - (j.Reference.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), + TimeSpan.Zero); + + foreach (var j2 in j.Collection) + { + j2.TestDateTimeOffset = new DateTimeOffset( + j2.TestDateTimeOffset.Ticks - (j2.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + } + + j.TestDateTimeOffsetCollection = j.TestDateTimeOffsetCollection.Select( + dto => new DateTimeOffset(dto.Ticks - (dto.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero)).ToList(); + } + + 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.GaussDB.FunctionalTests/Query/JsonStringQueryTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/JsonStringQueryTest.cs index b8b8909209..5e46fbb304 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonStringQueryTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/JsonStringQueryTest.cs @@ -362,7 +362,7 @@ protected override string StoreName => "JsonStringQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/LegacyGaussDBNodaTimeTypeMappingTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/LegacyGaussDBNodaTimeTypeMappingTest.cs new file mode 100644 index 0000000000..93fa9eb64d --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/LegacyGaussDBNodaTimeTypeMappingTest.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore.Storage.Json; +using NodaTime; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +#if DEBUG + +namespace Microsoft.EntityFrameworkCore.Query +{ + [Collection("LegacyNodaTimeTest")] + public class LegacyGaussDBNodaTimeTypeMappingTest + : 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 = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange))!; + Assert.Equal("tsrange", mapping.StoreType); + Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange( + 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 GaussDBTypeMappingSource Mapper = new( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + []), + new RelationalTypeMappingSourceDependencies( + [ + new GaussDBNodaTimeTypeMappingSourcePlugin( + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies())) + ]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions() + ); + + 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 LegacyGaussDBNodaTimeTypeMappingFixture : IDisposable + { + public LegacyGaussDBNodaTimeTypeMappingFixture() + { + GaussDBNodaTimeTypeMappingSourcePlugin.LegacyTimestampBehavior = true; + } + + public void Dispose() + => GaussDBNodaTimeTypeMappingSourcePlugin.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.GaussDB.FunctionalTests/Query/LegacyTimestampQueryTest.cs similarity index 95% rename from test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/LegacyTimestampQueryTest.cs index 1f64419401..031ee60306 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LegacyTimestampQueryTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/LegacyTimestampQueryTest.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; #if DEBUG @@ -100,19 +100,19 @@ protected override string StoreName => "TimestampQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; public LegacyTimestampQueryFixture() { - NpgsqlTypeMappingSource.LegacyTimestampBehavior = true; + GaussDBTypeMappingSource.LegacyTimestampBehavior = true; } public override Task DisposeAsync() { - NpgsqlTypeMappingSource.LegacyTimestampBehavior = false; + GaussDBTypeMappingSource.LegacyTimestampBehavior = false; return Task.CompletedTask; } diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyNoTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyNoTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..f46ca4266c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyNoTrackingQueryGaussDBTest.cs @@ -0,0 +1,16 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +internal class ManyToManyNoTrackingQueryGaussDBTest + : ManyToManyNoTrackingQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public ManyToManyNoTrackingQueryGaussDBTest(ManyToManyQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // TODO: #1232 + // protected override bool CanExecuteQueryString => true; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyQueryGaussDBFixture.cs new file mode 100644 index 0000000000..fc1d7402bb --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyQueryGaussDBFixture.cs @@ -0,0 +1,178 @@ +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ManyToManyQueryGaussDBFixture : ManyToManyQueryRelationalFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. So we need to explicitly set some column types to 'timestamp without time zone', but this is difficult/problematic + // for some of the many-to-many join entities configured below. So for now we duplicate the entire method. + // TODO: https://github.com/dotnet/efcore/issues/26068 + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().HasKey( + e => new + { + e.Key1, + e.Key2, + e.Key3 + }); + + // GaussDB customization: + modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); + + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().HasBaseType(); + modelBuilder.Entity().HasBaseType(); + + modelBuilder.Entity() + .HasMany(e => e.Collection) + .WithOne(e => e.CollectionInverse) + .HasForeignKey(e => e.CollectionInverseId); + + modelBuilder.Entity() + .HasOne(e => e.Reference) + .WithOne(e => e.ReferenceInverse) + .HasForeignKey(e => e.ReferenceInverseId); + + modelBuilder.Entity() + .HasMany(e => e.TwoSkipShared) + .WithMany(e => e.OneSkipShared); + + // Nav:2 Payload:No Join:Concrete Extra:None + modelBuilder.Entity() + .HasMany(e => e.TwoSkip) + .WithMany(e => e.OneSkip) + .UsingEntity(); + + // Nav:6 Payload:Yes Join:Concrete Extra:None + modelBuilder.Entity() + .HasMany(e => e.ThreeSkipPayloadFull) + .WithMany(e => e.OneSkipPayloadFull) + .UsingEntity( + r => r.HasOne(x => x.Three).WithMany(e => e.JoinOnePayloadFull), + l => l.HasOne(x => x.One).WithMany(e => e.JoinThreePayloadFull)); + + // Nav:4 Payload:Yes Join:Shared Extra:None + modelBuilder.Entity() + .HasMany(e => e.ThreeSkipPayloadFullShared) + .WithMany(e => e.OneSkipPayloadFullShared) + .UsingEntity>( + "JoinOneToThreePayloadFullShared", + r => r.HasOne().WithMany(e => e.JoinOnePayloadFullShared).HasForeignKey("ThreeId"), + l => l.HasOne().WithMany(e => e.JoinThreePayloadFullShared).HasForeignKey("OneId")) + .IndexerProperty("Payload"); + + // Nav:6 Payload:Yes Join:Concrete Extra:Self-Ref + modelBuilder.Entity() + .HasMany(e => e.SelfSkipPayloadLeft) + .WithMany(e => e.SelfSkipPayloadRight) + .UsingEntity( + l => l.HasOne(x => x.Left).WithMany(x => x.JoinSelfPayloadLeft), + r => r.HasOne(x => x.Right).WithMany(x => x.JoinSelfPayloadRight), + // GaussDB customization + x => x.Property(e => e.Payload).HasColumnType("timestamp without time zone")); + + // Nav:2 Payload:No Join:Concrete Extra:Inheritance + modelBuilder.Entity() + .HasMany(e => e.BranchSkip) + .WithMany(e => e.OneSkip) + .UsingEntity(); + + modelBuilder.Entity() + .HasOne(e => e.Reference) + .WithOne(e => e.ReferenceInverse) + .HasForeignKey(e => e.ReferenceInverseId); + + modelBuilder.Entity() + .HasMany(e => e.Collection) + .WithOne(e => e.CollectionInverse) + .HasForeignKey(e => e.CollectionInverseId); + + // Nav:6 Payload:No Join:Concrete Extra:None + modelBuilder.Entity() + .HasMany(e => e.ThreeSkipFull) + .WithMany(e => e.TwoSkipFull) + .UsingEntity( + r => r.HasOne(x => x.Three).WithMany(e => e.JoinTwoFull), + l => l.HasOne(x => x.Two).WithMany(e => e.JoinThreeFull)); + + // Nav:2 Payload:No Join:Shared Extra:Self-ref + modelBuilder.Entity() + .HasMany(e => e.SelfSkipSharedLeft) + .WithMany(e => e.SelfSkipSharedRight); + + // Nav:2 Payload:No Join:Shared Extra:CompositeKey + modelBuilder.Entity() + .HasMany(e => e.CompositeKeySkipShared) + .WithMany(e => e.TwoSkipShared); + + // Nav:6 Payload:No Join:Concrete Extra:CompositeKey + modelBuilder.Entity() + .HasMany(e => e.CompositeKeySkipFull) + .WithMany(e => e.ThreeSkipFull) + .UsingEntity( + l => l.HasOne(x => x.Composite).WithMany(x => x.JoinThreeFull).HasForeignKey( + e => new + { + e.CompositeId1, + e.CompositeId2, + e.CompositeId3 + }).IsRequired(), + r => r.HasOne(x => x.Three).WithMany(x => x.JoinCompositeKeyFull).IsRequired()); + + // Nav:2 Payload:No Join:Shared Extra:Inheritance + modelBuilder.Entity() + .HasMany(e => e.RootSkipShared) + .WithMany(e => e.ThreeSkipShared); + + // Nav:2 Payload:No Join:Shared Extra:Inheritance,CompositeKey + modelBuilder.Entity() + .HasMany(e => e.RootSkipShared) + .WithMany(e => e.CompositeKeySkipShared); + + // Nav:6 Payload:No Join:Concrete Extra:Inheritance,CompositeKey + modelBuilder.Entity() + .HasMany(e => e.LeafSkipFull) + .WithMany(e => e.CompositeKeySkipFull) + .UsingEntity( + r => r.HasOne(x => x.Leaf).WithMany(x => x.JoinCompositeKeyFull), + l => l.HasOne(x => x.Composite).WithMany(x => x.JoinLeafFull).HasForeignKey( + e => new + { + e.CompositeId1, + e.CompositeId2, + e.CompositeId3 + }), + x => + { + // GaussDB customization + x.Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + }); + + modelBuilder.SharedTypeEntity( + "PST", b => + { + b.IndexerProperty("Id").ValueGeneratedNever(); + b.IndexerProperty("Payload"); + }); + } + + // protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + // { + // base.OnModelCreating(modelBuilder, context); + // + // // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // // supported. + // modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); + // modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + // modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); + // modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + // } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyQueryGaussDBTest.cs new file mode 100644 index 0000000000..10c0ab4524 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ManyToManyQueryGaussDBTest.cs @@ -0,0 +1,12 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +internal class ManyToManyQueryGaussDBTest : ManyToManyQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public ManyToManyQueryGaussDBTest(ManyToManyQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/MappingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/MappingQueryGaussDBTest.cs new file mode 100644 index 0000000000..cba42ceac3 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/MappingQueryGaussDBTest.cs @@ -0,0 +1,36 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +// ReSharper disable once UnusedMember.Global +public class MappingQueryGaussDBTest : MappingQueryTestBase +{ + public MappingQueryGaussDBTest(MappingQueryGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public class MappingQueryGaussDBFixture : MappingQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBNorthwindTestStoreFactory.Instance; + + protected override string DatabaseSchema { get; } = "public"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + e => + { + e.Property(c => c.CompanyName2).Metadata.SetColumnName("CompanyName"); + e.Metadata.SetTableName("Customers"); + e.Metadata.SetSchema("public"); + }); + + modelBuilder.Entity() + .Property(c => c.EmployeeID) + .HasColumnType("int"); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NavigationTest.cs similarity index 93% rename from test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/NavigationTest.cs index 3d0be38023..9775c3bb4d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NavigationTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NavigationTest.cs @@ -80,13 +80,13 @@ public class NavigationTestFixture public NavigationTestFixture() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); - var connStrBuilder = new NpgsqlConnectionStringBuilder(TestEnvironment.DefaultConnection) { Database = "StateManagerBug" }; + var connStrBuilder = new GaussDBConnectionStringBuilder(TestEnvironment.DefaultConnection) { Database = "StateManagerBug" }; _options = new DbContextOptionsBuilder() - .UseNpgsql(connStrBuilder.ConnectionString) + .UseGaussDB(connStrBuilder.ConnectionString) .UseInternalServiceProvider(serviceProvider) .Options; } diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryGaussDBTest.cs new file mode 100644 index 0000000000..2bf953cd48 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryGaussDBTest.cs @@ -0,0 +1,107 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NonSharedPrimitiveCollectionsQueryGaussDBTest(NonSharedFixture fixture) + : NonSharedPrimitiveCollectionsQueryRelationalTestBase(fixture) +{ + protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode) + { + new GaussDBDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); + + return optionsBuilder; + } + + #region Support for specific element types + + // Since we just use arrays for primitive collections, there's no need to test each and every element type; arrays are fully typed + // and don't need any special conversion/handling like in providers which use JSON. + + // GaussDB maps DateTime to timestamp with time zone by default, which requires UTC timestamps. + public override Task Array_of_DateTime() + => TestArray( + new DateTime(2023, 1, 1, 12, 30, 0), + new DateTime(2023, 1, 2, 12, 30, 0), + mb => mb.Entity() + .Property(typeof(DateTime[]), "SomeArray") + .HasColumnType("timestamp without time zone[]")); + + // GaussDB maps DateTime to timestamp with time zone by default, which requires UTC timestamps. + public override Task Array_of_DateTime_with_milliseconds() + => TestArray( + new DateTime(2023, 1, 1, 12, 30, 0, 123), + new DateTime(2023, 1, 1, 12, 30, 0, 124), + mb => mb.Entity() + .Property(typeof(DateTime[]), "SomeArray") + .HasColumnType("timestamp without time zone[]")); + + // GaussDB maps DateTime to timestamp with time zone by default, which requires UTC timestamps. + public override Task Array_of_DateTime_with_microseconds() + => TestArray( + new DateTime(2023, 1, 1, 12, 30, 0, 123, 456), + new DateTime(2023, 1, 1, 12, 30, 0, 123, 457), + mb => mb.Entity() + .Property(typeof(DateTime[]), "SomeArray") + .HasColumnType("timestamp without time zone[]")); + + [ConditionalFact] + public virtual Task Array_of_DateTime_utc() + => TestArray( + new DateTime(2023, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new DateTime(2023, 1, 2, 12, 30, 0, DateTimeKind.Utc)); + + // GaussDB only supports DateTimeOffset with Offset 0 (mapped to timestamp with time zone) + public override Task Array_of_DateTimeOffset() + => TestArray( + new DateTimeOffset(2023, 1, 1, 12, 30, 0, TimeSpan.Zero), + new DateTimeOffset(2023, 1, 2, 12, 30, 0, TimeSpan.Zero)); + + [ConditionalFact] + public override async Task Multidimensional_array_is_not_supported() + { + // Multidimensional arrays are supported in GaussDB (via the regular array type); the EFCore.GaussDB maps .NET + // multidimensional arrays. However, arrays of multidimensional arrays aren't supported (since arrays of arrays generally aren't + // supported). + var contextFactory = await InitializeAsync( + mb => mb.Entity().Property("MultidimensionalArray"), + seed: async context => + { + var entry = context.Add(new TestEntity()); + entry.Property("MultidimensionalArray").CurrentValue = new[,] { { 1, 2 }, { 3, 4 } }; + await context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateContext(); + + var arrays = new[] { new[,] { { 1, 2 }, { 3, 4 } }, new[,] { { 1, 2 }, { 3, 5 } } }; + + await Assert.ThrowsAsync( + () => + context.Set().Where(t => arrays.Contains(EF.Property(t, "MultidimensionalArray"))).ToArrayAsync()); + } + + #endregion Support for specific element types + + public override async Task 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 ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAggregateOperatorsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAggregateOperatorsQueryGaussDBTest.cs new file mode 100644 index 0000000000..fba7f82229 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAggregateOperatorsQueryGaussDBTest.cs @@ -0,0 +1,190 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindAggregateOperatorsQueryGaussDBTest : NorthwindAggregateOperatorsQueryRelationalTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindAggregateOperatorsQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // https://github.com/dotnet/efcore/issues/36311 + public override Task Contains_with_parameter_list_value_type_id(bool async) + => Assert.ThrowsAsync(() => base.Contains_with_parameter_list_value_type_id(async)); + + // https://github.com/dotnet/efcore/issues/36311 + public override Task IReadOnlySet_Contains_with_parameter(bool async) + => Assert.ThrowsAsync(() => base.IReadOnlySet_Contains_with_parameter(async)); + + // https://github.com/dotnet/efcore/issues/36311 + public override Task List_Contains_with_parameter_list(bool async) + => Assert.ThrowsAsync(() => base.List_Contains_with_parameter_list(async)); + + // https://github.com/dotnet/efcore/issues/36311 + public override Task IImmutableSet_Contains_with_parameter(bool async) + => Assert.ThrowsAsync(() => base.IImmutableSet_Contains_with_parameter(async)); + + // Overriding to add equality tolerance because of floating point precision + 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: (e, a) => Assert.Equal(e, a, 10)); + + AssertSql( + """ +@p='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 +) AS c0 +"""); + } + + public override async Task Contains_with_local_uint_array_closure(bool async) + { + await base.Contains_with_local_uint_array_closure(async); + + // Note: GaussDB doesn't support uint, but value converters make this into bigint + AssertSql( + """ +@ids={ '0', '1' } (DbType = Object) + +SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" +FROM "Employees" AS e +WHERE e."EmployeeID" = ANY (@ids) +""", + // + """ +@ids={ '0' } (DbType = Object) + +SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" +FROM "Employees" AS e +WHERE e."EmployeeID" = ANY (@ids) +"""); + } + + public override async Task Contains_with_local_nullable_uint_array_closure(bool async) + { + await base.Contains_with_local_nullable_uint_array_closure(async); + + // Note: GaussDB doesn't support uint, but value converters make this into bigint + + AssertSql( + """ +@ids={ '0', '1' } (DbType = Object) + +SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" +FROM "Employees" AS e +WHERE e."EmployeeID" = ANY (@ids) +""", + // + """ +@ids={ '0' } (DbType = Object) + +SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" +FROM "Employees" AS e +WHERE e."EmployeeID" = ANY (@ids) +"""); + } + + public override Task Contains_with_local_anonymous_type_array_closure(bool async) + // Aggregates. Issue #15937. + => AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); + + public override Task Contains_with_local_tuple_array_closure(bool async) + => Assert.ThrowsAsync(() => base.Contains_with_local_tuple_array_closure(async: true)); + + public override async Task Contains_with_local_enumerable_inline(bool async) + { + // Issue #31776 + await Assert.ThrowsAsync( + async () => + await base.Contains_with_local_enumerable_inline(async)); + + AssertSql(); + } + + public override async Task Contains_with_local_enumerable_inline_closure_mix(bool async) + { + await base.Contains_with_local_enumerable_inline_closure_mix(async); + + AssertSql( + """ +@p={ '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, NULL)) +""", + // + """ +@p={ '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, NULL)) +"""); + } + + public override async Task Contains_with_local_non_primitive_list_closure_mix(bool async) + { + await base.Contains_with_local_non_primitive_list_closure_mix(async); + + AssertSql( + """ +@Select={ '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 (@Select) +"""); + } + + public override async Task Contains_with_local_non_primitive_list_inline_closure_mix(bool async) + { + await base.Contains_with_local_non_primitive_list_inline_closure_mix(async); + + AssertSql( + """ +@Select={ '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 (@Select) +""", + // + """ +@Select={ '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 (@Select) +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAsNoTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAsNoTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..b31c7493ca --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAsNoTrackingQueryGaussDBTest.cs @@ -0,0 +1,12 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindAsNoTrackingQueryGaussDBTest : NorthwindAsNoTrackingQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindAsNoTrackingQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAsTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAsTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..de284e8cb9 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindAsTrackingQueryGaussDBTest.cs @@ -0,0 +1,12 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindAsTrackingQueryGaussDBTest : NorthwindAsTrackingQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindAsTrackingQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindChangeTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindChangeTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..08fd48061f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindChangeTrackingQueryGaussDBTest.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindChangeTrackingQueryGaussDBTest : NorthwindChangeTrackingQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindChangeTrackingQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override NorthwindContext CreateNoTrackingContext() + => new NorthwindGaussDBContext( + new DbContextOptionsBuilder(Fixture.CreateOptions()) + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking).Options); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindCompiledQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindCompiledQueryGaussDBTest.cs new file mode 100644 index 0000000000..782b247a4b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindCompiledQueryGaussDBTest.cs @@ -0,0 +1,14 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindCompiledQueryGaussDBTest : NorthwindCompiledQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindCompiledQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindDbFunctionsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindDbFunctionsQueryGaussDBTest.cs new file mode 100644 index 0000000000..6b99578683 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindDbFunctionsQueryGaussDBTest.cs @@ -0,0 +1,263 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class NorthwindDbFunctionsQueryGaussDBTest : NorthwindDbFunctionsQueryRelationalTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindDbFunctionsQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Like / ILike + + public override async Task Like_literal(bool async) + { + // GaussDB like is case-sensitive, while the EF Core "default" (i.e. SqlServer) is insensitive. + // So we override and assert only 19 matches unlike the default's 34. + await AssertCount( + async, + ss => ss.Set(), + ss => ss.Set(), + c => EF.Functions.Like(c.ContactName, "%M%"), + c => c.ContactName.Contains("M")); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" LIKE '%M%' +"""); + } + + public override async Task Like_identity(bool async) + { + await base.Like_identity(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" LIKE c."ContactName" ESCAPE '' +"""); + } + + public override async Task Like_literal_with_escape(bool async) + { + await base.Like_literal_with_escape(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" LIKE '!%' ESCAPE '!' +"""); + } + + [Fact] + public void String_Like_Literal_With_Backslash() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => EF.Functions.Like(c.ContactName, "\\")); + + Assert.Equal(0, count); + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" LIKE '\' ESCAPE '' +"""); + } + + [Fact] + public void String_ILike_Literal() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => EF.Functions.ILike(c.ContactName, "%M%")); + + Assert.Equal(34, count); + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" ILIKE '%M%' +"""); + } + + [Fact] + public void String_ILike_Literal_With_Escape() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => EF.Functions.ILike(c.ContactName, "!%", "!")); + + Assert.Equal(0, count); + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +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 + + [MinimumPostgresVersion(12, 0)] + [PlatformSkipCondition(TestUtilities.Xunit.TestPlatform.Windows, SkipReason = "ICU non-deterministic doesn't seem to work on Windows?")] + public override async Task Collate_case_insensitive(bool async) + { + await base.Collate_case_insensitive(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" COLLATE "some-case-insensitive-collation" = 'maria anders' +"""); + } + + public override async Task Collate_case_sensitive(bool async) + { + await base.Collate_case_sensitive(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE c."ContactName" COLLATE "POSIX" = 'maria anders' +"""); + } + + protected override string CaseInsensitiveCollation + => "some-case-insensitive-collation"; + + protected override string CaseSensitiveCollation + => "POSIX"; + + #endregion Collation + + #region Others + + [Fact] + public void Distance_with_timestamp() + { + using var context = CreateContext(); + var closestOrder = context.Orders.OrderBy(o => EF.Functions.Distance(o.OrderDate.Value, new DateTime(1997, 06, 28))).First(); + + Assert.Equal(10582, closestOrder.OrderID); + + AssertSql( + """ +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Orders" AS o +ORDER BY o."OrderDate" <-> TIMESTAMP '1997-06-28T00:00:00' NULLS FIRST +LIMIT 1 +"""); + } + + [Fact] + public void String_reverse() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => EF.Functions.Reverse(c.ContactName) == "srednA airaM"); + + Assert.Equal(1, count); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE reverse(c."ContactName") = 'srednA airaM' +"""); + } + + // ReSharper disable once InconsistentNaming + [Fact] + public void StringToArray() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => EF.Functions.StringToArray(c.ContactName, " ") == new[] { "Maria", "Anders" }); + + Assert.Equal(1, count); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE string_to_array(c."ContactName", ' ') = ARRAY['Maria','Anders']::text[] +"""); + } + + [Fact] + public void StringToArray_with_null_string() + { + using var context = CreateContext(); + var count = context.Customers.Count(c => EF.Functions.StringToArray(c.ContactName, " ", "Maria") == new[] { null, "Anders" }); + + Assert.Equal(1, count); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE string_to_array(c."ContactName", ' ', 'Maria') = ARRAY[NULL,'Anders']::text[] +"""); + } + + [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) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryGaussDBTest.cs new file mode 100644 index 0000000000..fd23d95f96 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryGaussDBTest.cs @@ -0,0 +1,25 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindEFPropertyIncludeQueryGaussDBTest : NorthwindEFPropertyIncludeQueryTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindEFPropertyIncludeQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public override async Task Include_collection_with_last_no_orderby(bool async) + { + Assert.Equal( + RelationalStrings.LastUsedWithoutOrderBy(nameof(Enumerable.Last)), + (await Assert.ThrowsAsync( + () => base.Include_collection_with_last_no_orderby(async))).Message); + + AssertSql(); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindFunctionsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindFunctionsQueryGaussDBTest.cs new file mode 100644 index 0000000000..f1dab06f02 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindFunctionsQueryGaussDBTest.cs @@ -0,0 +1,1062 @@ +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class NorthwindFunctionsQueryGaussDBTest : NorthwindFunctionsQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindFunctionsQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Substring + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task Substring_without_length_with_Index_of(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => x.Address == "Walserweg 21") + .Where(x => x.Address.Substring(x.Address.IndexOf("e")) == "erweg 21")); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task Substring_without_length_with_constant(bool async) + => AssertQuery( + async, + //Walserweg 21 + cs => cs.Set().Where(x => x.Address.Substring(5) == "rweg 21")); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task Substring_without_length_with_closure(bool async) + { + var startIndex = 5; + return AssertQuery( + async, + //Walserweg 21 + ss => ss.Set().Where(x => x.Address.Substring(startIndex) == "rweg 21")); + } + + #endregion + + #region Regex + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_with_constant_pattern(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_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) + { + var pattern = "^A"; + + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, pattern))); + + AssertSql( + """ +@pattern='^A' + +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)' || @pattern) +"""); + } + + [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) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.None))); + + 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_IgnoreCase(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_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) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.Multiline))); + + 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" ~ '(?n)^A' +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_with_Singleline(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.Singleline))); + + 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" ~ '^A' +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_with_Singleline_and_IgnoreCase(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^a", RegexOptions.Singleline | 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" ~* '^a' +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_IsMatch_with_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^ A", RegexOptions.IgnorePatternWhitespace))); + + 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" ~ '(?px)^ A' +"""); + } + + [Fact] + public void Regex_IsMatch_with_unsupported_option() + => Assert.Throws( + () => + Fixture.CreateContext().Customers.Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.RightToLeft)).ToList()); + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_constant_pattern_and_replacement(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B"))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') +FROM "Customers" AS c +""" + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_parameter_pattern_and_replacement(bool async) + { + var pattern = "^A"; + var replacement = "B"; + + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, pattern, replacement))); + + AssertSql( + """ +@pattern='^A' +@replacement='B' + +SELECT regexp_replace(c."CompanyName", @pattern, @replacement, 'p') +FROM "Customers" AS c +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_OptionsNone(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.None))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') +FROM "Customers" AS c +""" + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_IgnoreCase(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.IgnoreCase))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^a', 'B', 'pi') +FROM "Customers" AS c +""" + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_Multiline(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.Multiline))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^A', 'B', 'n') +FROM "Customers" AS c +""" + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_Singleline(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.Singleline))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^A', 'B') +FROM "Customers" AS c +""" + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_Singleline_and_IgnoreCase(bool async) + { + await AssertQuery( + async, + source => source.Set() + .Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.Singleline | RegexOptions.IgnoreCase))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^a', 'B', 'i') +FROM "Customers" AS c +""" + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^ A", "B", RegexOptions.IgnorePatternWhitespace))); + + AssertSql( + """ +SELECT regexp_replace(c."CompanyName", '^ A', 'B', 'px') +FROM "Customers" AS c +""" + ); + } + + [Fact] + public void Regex_Replace_with_unsupported_option() + => Assert.Throws( + () => Fixture.CreateContext().Customers + .FirstOrDefault(x => Regex.Replace(x.CompanyName, "^A", "foo", RegexOptions.RightToLeft) != null)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_constant_pattern(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A"))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^A', 1, 'p') +FROM "Customers" AS c +""" + ); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_parameter_pattern(bool async) + { + var pattern = "^A"; + + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, pattern))); + + AssertSql( + """ +@pattern='^A' + +SELECT regexp_count(c."CompanyName", @pattern, 1, 'p') +FROM "Customers" AS c +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_OptionsNone(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.None))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^A', 1, 'p') +FROM "Customers" AS c +""" + ); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_IgnoreCase(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^a", RegexOptions.IgnoreCase))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^a', 1, 'pi') +FROM "Customers" AS c +""" + ); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_Multiline(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.Multiline))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^A', 1, 'n') +FROM "Customers" AS c +""" + ); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_Singleline(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.Singleline))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^A') +FROM "Customers" AS c +""" + ); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_Singleline_and_IgnoreCase(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^a", RegexOptions.Singleline | RegexOptions.IgnoreCase))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^a', 1, 'i') +FROM "Customers" AS c +""" + ); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^ A", RegexOptions.IgnorePatternWhitespace))); + + AssertSql( + """ +SELECT regexp_count(c."CompanyName", '^ A', 1, 'px') +FROM "Customers" AS c +""" + ); + } + + [ConditionalFact] + [MinimumPostgresVersion(15, 0)] + public void Regex_Count_with_unsupported_option() + => Assert.Throws( + () => Fixture.CreateContext().Customers + .FirstOrDefault(x => Regex.Count(x.CompanyName, "^A", RegexOptions.RightToLeft) != 0)); + + #endregion Regex + + #region PadLeft, PadRight + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadLeft_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadLeft(20).EndsWith("Walserweg 21"))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadLeft_char_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadLeft(20, 'a').EndsWith("Walserweg 21"))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadLeft_with_parameter(bool async) + { + var length = 20; + + return AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadLeft(length).EndsWith("Walserweg 21"))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadLeft_char_with_parameter(bool async) + { + var length = 20; + + return AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadLeft(length, 'a').EndsWith("Walserweg 21"))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadRight_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadRight(20).StartsWith("Walserweg 21"))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadRight_char_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadRight(20).StartsWith("Walserweg 21"))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadRight_with_parameter(bool async) + { + var length = 20; + + return AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadRight(length).StartsWith("Walserweg 21"))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public Task PadRight_char_with_parameter(bool async) + { + var length = 20; + + return AssertQuery( + async, + ss => ss.Set().Where(x => x.Address.PadRight(length, 'a').StartsWith("Walserweg 21"))); + } + + #endregion + + #region Aggregate functions + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_ArrayAgg(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(c => c.City) + .Select(g => new { City = g.Key, FaxNumbers = EF.Functions.ArrayAgg(g.Select(c => c.Fax).OrderBy(id => id)) }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var london = results.Single(x => x.City == "London"); + Assert.Collection( + london.FaxNumbers, + Assert.Null, + f => Assert.Equal("(171) 555-2530", f), + f => Assert.Equal("(171) 555-3373", f), + f => Assert.Equal("(171) 555-5646", f), + f => Assert.Equal("(171) 555-6750", f), + f => Assert.Equal("(171) 555-9199", f)); + + AssertSql( + """ +SELECT c."City", array_agg(c."Fax" ORDER BY c."Fax" NULLS FIRST) AS "FaxNumbers" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_JsonAgg(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(c => c.City) + .Select(g => new { City = g.Key, FaxNumbers = EF.Functions.JsonAgg(g.Select(c => c.Fax).OrderBy(id => id)) }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var london = results.Single(x => x.City == "London"); + Assert.Collection( + london.FaxNumbers, + Assert.Null, + f => Assert.Equal("(171) 555-2530", f), + f => Assert.Equal("(171) 555-3373", f), + f => Assert.Equal("(171) 555-5646", f), + f => Assert.Equal("(171) 555-6750", f), + f => Assert.Equal("(171) 555-9199", f)); + + AssertSql( + """ +SELECT c."City", json_agg(c."Fax" ORDER BY c."Fax" NULLS FIRST) AS "FaxNumbers" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_JsonbAgg(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(c => c.City) + .Select(g => new { City = g.Key, FaxNumbers = EF.Functions.JsonbAgg(g.Select(c => c.Fax).OrderBy(id => id)) }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var london = results.Single(x => x.City == "London"); + Assert.Collection( + london.FaxNumbers, + Assert.Null, + f => Assert.Equal("(171) 555-2530", f), + f => Assert.Equal("(171) 555-3373", f), + f => Assert.Equal("(171) 555-5646", f), + f => Assert.Equal("(171) 555-6750", f), + f => Assert.Equal("(171) 555-9199", f)); + + AssertSql( + """ +SELECT c."City", jsonb_agg(c."Fax" ORDER BY c."Fax" NULLS FIRST) AS "FaxNumbers" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + #endregion Aggregate functions + + #region JsonObjectAgg + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_JsonObjectAgg(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(c => c.City) + .Select( + g => new + { + City = g.Key, + Companies = EF.Functions.JsonObjectAgg( + g + .OrderBy(c => c.CompanyName) + .Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + 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" }""", + london.Companies); + + AssertSql( + """ +SELECT c."City", json_object_agg(c."CompanyName", c."ContactName" ORDER BY c."CompanyName" NULLS FIRST) AS "Companies" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_JsonObjectAgg_as_Dictionary(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(c => c.City) + .Select( + g => new + { + City = g.Key, + Companies = EF.Functions.JsonObjectAgg>( + g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var london = results.Single(r => r.City == "London"); + + Assert.Equal( + new Dictionary + { + ["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( + """ +SELECT c."City", json_object_agg(c."CompanyName", c."ContactName") AS "Companies" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_JsonbObjectAgg(bool async) + { + await using var ctx = CreateContext(); + + // Note that unlike with json, jsonb doesn't guarantee ordering; so we parse the JSON string client-side. + var query = ctx.Set() + .GroupBy(c => c.City) + .Select( + g => new + { + City = g.Key, + Companies = EF.Functions.JsonbObjectAgg(g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var london = results.Single(r => r.City == "London"); + var companiesDictionary = JsonSerializer.Deserialize>(london.Companies); + + Assert.Equal( + new Dictionary + { + ["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" + }, + companiesDictionary); + + AssertSql( + """ +SELECT c."City", jsonb_object_agg(c."CompanyName", c."ContactName") AS "Companies" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_JsonbObjectAgg_as_Dictionary(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(c => c.City) + .Select( + g => new + { + City = g.Key, + Companies = EF.Functions.JsonbObjectAgg>( + g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var london = results.Single(r => r.City == "London"); + + Assert.Equal( + new Dictionary + { + ["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( + """ +SELECT c."City", jsonb_object_agg(c."CompanyName", c."ContactName") AS "Companies" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + #endregion JsonObjectAgg + + #region Statistics + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task StandardDeviation(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(od => od.ProductID) + .Select( + g => new + { + ProductID = g.Key, + SampleStandardDeviation = EF.Functions.StandardDeviationSample(g.Select(od => od.UnitPrice)), + PopulationStandardDeviation = EF.Functions.StandardDeviationPopulation(g.Select(od => od.UnitPrice)) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var product9 = results.Single(r => r.ProductID == 9); + Assert.Equal(8.675943752699023, product9.SampleStandardDeviation.Value, 5); + Assert.Equal(7.759999999999856, product9.PopulationStandardDeviation.Value, 5); + + AssertSql( + """ +SELECT o."ProductID", stddev_samp(o."UnitPrice") AS "SampleStandardDeviation", stddev_pop(o."UnitPrice") AS "PopulationStandardDeviation" +FROM "Order Details" AS o +GROUP BY o."ProductID" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Variance(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(od => od.ProductID) + .Select( + g => new + { + ProductID = g.Key, + SampleStandardDeviation = EF.Functions.VarianceSample(g.Select(od => od.UnitPrice)), + PopulationStandardDeviation = EF.Functions.VariancePopulation(g.Select(od => od.UnitPrice)) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var product9 = results.Single(r => r.ProductID == 9); + Assert.Equal(75.2719999999972, product9.SampleStandardDeviation.Value, 5); + Assert.Equal(60.217599999997766, product9.PopulationStandardDeviation.Value, 5); + + AssertSql( + """ +SELECT o."ProductID", var_samp(o."UnitPrice") AS "SampleStandardDeviation", var_pop(o."UnitPrice") AS "PopulationStandardDeviation" +FROM "Order Details" AS o +GROUP BY o."ProductID" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Other_statistics_functions(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(od => od.ProductID) + .Select( + g => new + { + ProductID = g.Key, + Correlation = EF.Functions.Correlation(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + CovariancePopulation = + EF.Functions.CovariancePopulation(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + CovarianceSample = + EF.Functions.CovarianceSample(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrAverageX = EF.Functions.RegrAverageX(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrAverageY = EF.Functions.RegrAverageY(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrCount = EF.Functions.RegrCount(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrIntercept = EF.Functions.RegrIntercept(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrR2 = EF.Functions.RegrR2(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrSlope = EF.Functions.RegrSlope(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrSXX = EF.Functions.RegrSXX(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + RegrSXY = EF.Functions.RegrSXY(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var product9 = results.Single(r => r.ProductID == 9); + Assert.Equal(0.9336470941441423, product9.Correlation.Value, 5); + Assert.Equal(1.4799999967217445, product9.CovariancePopulation.Value, 5); + Assert.Equal(1.8499999959021807, product9.CovarianceSample.Value, 5); + Assert.Equal(0.10000000149011612, product9.RegrAverageX.Value, 5); + Assert.Equal(19, product9.RegrAverageY.Value, 5); + Assert.Equal(5, product9.RegrCount.Value); + Assert.Equal(2.5555555647538144, product9.RegrIntercept.Value, 5); + Assert.Equal(0.871696896403801, product9.RegrR2.Value, 5); + Assert.Equal(164.44444190204874, product9.RegrSlope.Value, 5); + Assert.Equal(0.045000000596046474, product9.RegrSXX.Value, 5); + Assert.Equal(7.399999983608723, product9.RegrSXY.Value, 5); + + AssertSql( + """ +SELECT o."ProductID", corr(o."Quantity"::double precision, o."Discount"::double precision) AS "Correlation", covar_pop(o."Quantity"::double precision, o."Discount"::double precision) AS "CovariancePopulation", covar_samp(o."Quantity"::double precision, o."Discount"::double precision) AS "CovarianceSample", regr_avgx(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrAverageX", regr_avgy(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrAverageY", regr_count(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrCount", regr_intercept(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrIntercept", regr_r2(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrR2", regr_slope(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrSlope", regr_sxx(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrSXX", regr_sxy(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrSXY" +FROM "Order Details" AS o +GROUP BY o."ProductID" +"""); + } + + #endregion Statistics + + #region NullIf + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task NullIf_with_equality_left_sided(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(x => x.OrderID == 1 ? (int?)null : x.OrderID)); + + AssertSql( + """ +SELECT NULLIF(o."OrderID", 1) +FROM "Orders" AS o +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task NullIf_with_equality_right_sided(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(x => 1 == x.OrderID ? (int?)null : x.OrderID)); + + AssertSql( + """ +SELECT NULLIF(o."OrderID", 1) +FROM "Orders" AS o +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task NullIf_with_inequality_left_sided(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(x => x.OrderID != 1 ? x.OrderID : (int?)null)); + + AssertSql( + """ +SELECT NULLIF(o."OrderID", 1) +FROM "Orders" AS o +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task NullIf_with_inequality_right_sided(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(x => 1 != x.OrderID ? x.OrderID : (int?)null)); + + AssertSql( + """ +SELECT NULLIF(o."OrderID", 1) +FROM "Orders" AS o +"""); + } + + #endregion + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindGroupByQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindGroupByQueryGaussDBTest.cs new file mode 100644 index 0000000000..d6a36c49a6 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindGroupByQueryGaussDBTest.cs @@ -0,0 +1,3889 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindGroupByQueryGaussDBTest : NorthwindGroupByQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindGroupByQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + #region GroupByProperty + + public override async Task GroupBy_Property_Select_Average(bool async) + { + await base.GroupBy_Property_Select_Average(async); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + + // Validating that we don't generate warning when translating GroupBy. See Issue#11157 + Assert.DoesNotContain( + "The LINQ expression 'GroupBy([o].CustomerID, [o])' could not be translated and will be evaluated locally.", + Fixture.TestSqlLoggerFactory.Log.Select(l => l.Message)); + } + + public override async Task GroupBy_Property_Select_Average_with_group_enumerable_projected(bool async) + { + await base.GroupBy_Property_Select_Average_with_group_enumerable_projected(async); + + AssertSql(); + } + + public override async Task GroupBy_Property_Select_Count(bool async) + { + await base.GroupBy_Property_Select_Count(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_LongCount(bool async) + { + await base.GroupBy_Property_Select_LongCount(async); + + AssertSql( + """ +SELECT count(*) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Count_with_nulls(bool async) + { + await base.GroupBy_Property_Select_Count_with_nulls(async); + + AssertSql( + """ +SELECT c."City", count(*)::int AS "Faxes" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + public override async Task GroupBy_Property_Select_LongCount_with_nulls(bool async) + { + await base.GroupBy_Property_Select_LongCount_with_nulls(async); + + AssertSql( + """ +SELECT c."City", count(*) AS "Faxes" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + public override async Task GroupBy_Property_Select_Max(bool async) + { + await base.GroupBy_Property_Select_Max(async); + + AssertSql( + """ +SELECT max(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Min(bool async) + { + await base.GroupBy_Property_Select_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Sum(bool async) + { + await base.GroupBy_Property_Select_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_Property_Select_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_Average(bool async) + { + await base.GroupBy_Property_Select_Key_Average(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Average" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_Count(bool async) + { + await base.GroupBy_Property_Select_Key_Count(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_LongCount(bool async) + { + await base.GroupBy_Property_Select_Key_LongCount(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*) AS "LongCount" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_Max(bool async) + { + await base.GroupBy_Property_Select_Key_Max(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", max(o."OrderID") AS "Max" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_Min(bool async) + { + await base.GroupBy_Property_Select_Key_Min(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", min(o."OrderID") AS "Min" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_Sum(bool async) + { + await base.GroupBy_Property_Select_Key_Sum(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_Property_Select_Key_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_Property_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID" AS "Key", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_key_multiple_times_and_aggregate(bool async) + { + await base.GroupBy_Property_Select_key_multiple_times_and_aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key1", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Key_with_constant(bool async) + { + await base.GroupBy_Property_Select_Key_with_constant(async); + + AssertSql( + """ +SELECT o0."Name", o0."CustomerID" AS "Value", count(*)::int AS "Count" +FROM ( + SELECT o."CustomerID", 'CustomerID' AS "Name" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Name", o0."CustomerID" +"""); + } + + public override async Task GroupBy_aggregate_projecting_conditional_expression(bool async) + { + await base.GroupBy_aggregate_projecting_conditional_expression(async); + + AssertSql( + """ +SELECT o."OrderDate" AS "Key", CASE + WHEN count(*)::int = 0 THEN 1 + ELSE COALESCE(sum(CASE + WHEN o."OrderID" % 2 = 0 THEN 1 + ELSE 0 + END), 0)::int / count(*)::int +END AS "SomeValue" +FROM "Orders" AS o +GROUP BY o."OrderDate" +"""); + } + + public override async Task GroupBy_aggregate_projecting_conditional_expression_based_on_group_key(bool async) + { + await base.GroupBy_aggregate_projecting_conditional_expression_based_on_group_key(async); + + AssertSql( + """ +SELECT CASE + WHEN o."OrderDate" IS NULL THEN 'is null' + ELSE 'is not null' +END AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."OrderDate" +"""); + } + + #endregion GroupByProperty + + #region GroupByAnonymousAggregate + + public override async Task GroupBy_anonymous_Select_Average(bool async) + { + await base.GroupBy_anonymous_Select_Average(async); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_Select_Count(bool async) + { + await base.GroupBy_anonymous_Select_Count(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_Select_LongCount(bool async) + { + await base.GroupBy_anonymous_Select_LongCount(async); + + AssertSql( + """ +SELECT count(*) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_Select_Max(bool async) + { + await base.GroupBy_anonymous_Select_Max(async); + + AssertSql( + """ +SELECT max(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_Select_Min(bool async) + { + await base.GroupBy_anonymous_Select_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_Select_Sum(bool async) + { + await base.GroupBy_anonymous_Select_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_Select_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_anonymous_Select_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_anonymous_with_alias_Select_Key_Sum(bool async) + { + await base.GroupBy_anonymous_with_alias_Select_Key_Sum(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Composite_Select_Average(bool async) + { + await base.GroupBy_Composite_Select_Average(async); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision) +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Count(bool async) + { + await base.GroupBy_Composite_Select_Count(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_LongCount(bool async) + { + await base.GroupBy_Composite_Select_LongCount(async); + + AssertSql( + """ +SELECT count(*) +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Max(bool async) + { + await base.GroupBy_Composite_Select_Max(async); + + AssertSql( + """ +SELECT max(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Min(bool async) + { + await base.GroupBy_Composite_Select_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Sum(bool async) + { + await base.GroupBy_Composite_Select_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_Composite_Select_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_Average(bool async) + { + await base.GroupBy_Composite_Select_Key_Average(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", avg(o."OrderID"::double precision) AS "Average" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_Count(bool async) + { + await base.GroupBy_Composite_Select_Key_Count(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", count(*)::int AS "Count" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_LongCount(bool async) + { + await base.GroupBy_Composite_Select_Key_LongCount(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", count(*) AS "LongCount" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_Max(bool async) + { + await base.GroupBy_Composite_Select_Key_Max(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", max(o."OrderID") AS "Max" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_Min(bool async) + { + await base.GroupBy_Composite_Select_Key_Min(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", min(o."OrderID") AS "Min" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_Sum(bool async) + { + await base.GroupBy_Composite_Select_Key_Sum(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Key_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_Composite_Select_Key_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT o."CustomerID", o."EmployeeID", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_Composite_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID", o."EmployeeID", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Sum_Min_Key_flattened_Max_Avg(bool async) + { + await base.GroupBy_Composite_Select_Sum_Min_Key_flattened_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID", o."EmployeeID", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Dto_as_key_Select_Sum(bool async) + { + await base.GroupBy_Dto_as_key_Select_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", o."CustomerID", o."EmployeeID" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Dto_as_element_selector_Select_Sum(bool async) + { + await base.GroupBy_Dto_as_element_selector_Select_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."EmployeeID"::bigint), 0.0)::bigint AS "Sum", o."CustomerID" AS "Key" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Composite_Select_Dto_Sum_Min_Key_flattened_Max_Avg(bool async) + { + await base.GroupBy_Composite_Select_Dto_Sum_Min_Key_flattened_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID" AS "CustomerId", o."EmployeeID" AS "EmployeeId", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Composite_Select_Sum_Min_part_Key_flattened_Max_Avg(bool async) + { + await base.GroupBy_Composite_Select_Sum_Min_part_Key_flattened_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID", o."EmployeeID" +"""); + } + + public override async Task GroupBy_Constant_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_Constant_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_Constant_with_element_selector_Select_Sum(bool async) + { + await base.GroupBy_Constant_with_element_selector_Select_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", 2 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_Constant_with_element_selector_Select_Sum2(bool async) + { + await base.GroupBy_Constant_with_element_selector_Select_Sum2(async); + + AssertSql( + """ +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", 2 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_Constant_with_element_selector_Select_Sum3(bool async) + { + await base.GroupBy_Constant_with_element_selector_Select_Sum3(async); + + AssertSql( + """ +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", 2 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_Constant_with_element_selector_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_Constant_with_element_selector_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", o0."Key" +FROM ( + SELECT o."OrderID", 2 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_constant_with_where_on_grouping_with_aggregate_operators(bool async) + { + await base.GroupBy_constant_with_where_on_grouping_with_aggregate_operators(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."Key" +ORDER BY o0."Key" NULLS FIRST +"""); + } + + public override async Task GroupBy_param_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_param_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +@a='2' + +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 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_param_with_element_selector_Select_Sum(bool async) + { + await base.GroupBy_param_with_element_selector_Select_Sum(async); + + AssertSql( + """ +@a='2' + +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", @a AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_param_with_element_selector_Select_Sum2(bool async) + { + await base.GroupBy_param_with_element_selector_Select_Sum2(async); + + AssertSql( + """ +@a='2' + +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", @a AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_param_with_element_selector_Select_Sum3(bool async) + { + await base.GroupBy_param_with_element_selector_Select_Sum3(async); + + AssertSql( + """ +@a='2' + +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", @a AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_param_with_element_selector_Select_Sum_Min_Key_Max_Avg(bool async) + { + await base.GroupBy_param_with_element_selector_Select_Sum_Min_Key_Max_Avg(async); + + AssertSql( + """ +@a='2' + +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", o0."Key" +FROM ( + SELECT o."OrderID", @a AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_anonymous_key_type_mismatch_with_aggregate(bool async) + { + await base.GroupBy_anonymous_key_type_mismatch_with_aggregate(async); + + AssertSql( + """ +SELECT count(*)::int AS "I0", o0."I0" AS "I1" +FROM ( + SELECT date_part('year', o."OrderDate")::int AS "I0" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."I0" +ORDER BY o0."I0" NULLS FIRST +"""); + } + + #endregion GroupByAnonymousAggregate + + #region GroupByWithElementSelectorAggregate + + public override async Task GroupBy_Property_scalar_element_selector_Average(bool async) + { + await base.GroupBy_Property_scalar_element_selector_Average(async); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_scalar_element_selector_Count(bool async) + { + await base.GroupBy_Property_scalar_element_selector_Count(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_scalar_element_selector_LongCount(bool async) + { + await base.GroupBy_Property_scalar_element_selector_LongCount(async); + + AssertSql( + """ +SELECT count(*) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_scalar_element_selector_Max(bool async) + { + await base.GroupBy_Property_scalar_element_selector_Max(async); + + AssertSql( + """ +SELECT max(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_scalar_element_selector_Min(bool async) + { + await base.GroupBy_Property_scalar_element_selector_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_scalar_element_selector_Sum(bool async) + { + await base.GroupBy_Property_scalar_element_selector_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_scalar_element_selector_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_Property_scalar_element_selector_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_Average(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_Average(async); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_Count(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_Count(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_LongCount(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_LongCount(async); + + AssertSql( + """ +SELECT count(*) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_Max(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_Max(async); + + AssertSql( + """ +SELECT max(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_Min(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_Sum(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_anonymous_element_selector_Sum_Min_Max_Avg(bool async) + { + await base.GroupBy_Property_anonymous_element_selector_Sum_Min_Max_Avg(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."EmployeeID") AS "Min", max(o."EmployeeID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_element_selector_complex_aggregate(bool async) + { + await base.GroupBy_element_selector_complex_aggregate(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID" + 1), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_element_selector_complex_aggregate2(bool async) + { + await base.GroupBy_element_selector_complex_aggregate2(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID" + 1), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_element_selector_complex_aggregate3(bool async) + { + await base.GroupBy_element_selector_complex_aggregate3(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID" + 1), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_element_selector_complex_aggregate4(bool async) + { + await base.GroupBy_element_selector_complex_aggregate4(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID" + 1), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task Element_selector_with_case_block_repeated_inside_another_case_block_in_projection(bool async) + { + await base.Element_selector_with_case_block_repeated_inside_another_case_block_in_projection(async); + + AssertSql( + """ +SELECT o."OrderID", COALESCE(sum(CASE + WHEN o."CustomerID" = 'ALFKI' THEN CASE + WHEN o."OrderID" > 1000 THEN o."OrderID" + ELSE -o."OrderID" + END + ELSE -CASE + WHEN o."OrderID" > 1000 THEN o."OrderID" + ELSE -o."OrderID" + END +END), 0)::int AS "Aggregate" +FROM "Orders" AS o +GROUP BY o."OrderID" +"""); + } + + public override async Task GroupBy_conditional_properties(bool async) + { + await base.GroupBy_conditional_properties(async); + + AssertSql( + """ +SELECT o0."OrderMonth", o0."CustomerID" AS "Customer", count(*)::int AS "Count" +FROM ( + SELECT o."CustomerID", NULL AS "OrderMonth" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."OrderMonth", o0."CustomerID" +"""); + } + + #endregion GroupByWithElementSelectorAggregate + + #region GroupByAfterComposition + + public override async Task GroupBy_empty_key_Aggregate(bool async) + { + await base.GroupBy_empty_key_Aggregate(async); + + AssertSql( + """ +SELECT COALESCE(sum(o0."OrderID"), 0)::int +FROM ( + SELECT o."OrderID", 1 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_empty_key_Aggregate_Key(bool async) + { + await base.GroupBy_empty_key_Aggregate_Key(async); + + AssertSql( + """ +SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" +FROM ( + SELECT o."OrderID", 1 AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task OrderBy_GroupBy_Aggregate(bool async) + { + await base.OrderBy_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task OrderBy_Skip_GroupBy_Aggregate(bool async) + { + await base.OrderBy_Skip_GroupBy_Aggregate(async); + + AssertSql( + """ +@p='80' + +SELECT avg(o0."OrderID"::double precision) +FROM ( + SELECT o."OrderID", o."CustomerID" + FROM "Orders" AS o + ORDER BY o."OrderID" NULLS FIRST + OFFSET @p +) AS o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task OrderBy_Take_GroupBy_Aggregate(bool async) + { + await base.OrderBy_Take_GroupBy_Aggregate(async); + + AssertSql( + """ +@p='500' + +SELECT min(o0."OrderID") +FROM ( + SELECT o."OrderID", o."CustomerID" + FROM "Orders" AS o + ORDER BY o."OrderID" NULLS FIRST + LIMIT @p +) AS o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task OrderBy_Skip_Take_GroupBy_Aggregate(bool async) + { + await base.OrderBy_Skip_Take_GroupBy_Aggregate(async); + + AssertSql( + """ +@p0='500' +@p='80' + +SELECT max(o0."OrderID") +FROM ( + SELECT o."OrderID", o."CustomerID" + FROM "Orders" AS o + ORDER BY o."OrderID" NULLS FIRST + LIMIT @p0 OFFSET @p +) AS o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task Distinct_GroupBy_Aggregate(bool async) + { + await base.Distinct_GroupBy_Aggregate(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task Anonymous_projection_Distinct_GroupBy_Aggregate(bool async) + { + await base.Anonymous_projection_Distinct_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT o0."EmployeeID" AS "Key", count(*)::int AS c +FROM ( + SELECT DISTINCT o."OrderID", o."EmployeeID" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."EmployeeID" +"""); + } + + public override async Task SelectMany_GroupBy_Aggregate(bool async) + { + await base.SelectMany_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT o."EmployeeID" AS "Key", count(*)::int AS c +FROM "Customers" AS c +INNER JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +GROUP BY o."EmployeeID" +"""); + } + + public override async Task Join_GroupBy_Aggregate(bool async) + { + await base.Join_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT c."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Count" +FROM "Orders" AS o +INNER JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY c."CustomerID" +"""); + } + + public override async Task GroupBy_required_navigation_member_Aggregate(bool async) + { + await base.GroupBy_required_navigation_member_Aggregate(async); + + AssertSql( + """ +SELECT o0."CustomerID" AS "CustomerId", count(*)::int AS "Count" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +GROUP BY o0."CustomerID" +"""); + } + + public override async Task Join_complex_GroupBy_Aggregate(bool async) + { + await base.Join_complex_GroupBy_Aggregate(async); + + AssertSql( + """ +@p='100' +@p1='50' +@p0='10' + +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 +) 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 @p1 OFFSET @p0 +) AS c0 ON o0."CustomerID" = c0."CustomerID" +GROUP BY c0."CustomerID" +"""); + } + + public override async Task GroupJoin_GroupBy_Aggregate(bool async) + { + await base.GroupJoin_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Average" +FROM "Customers" AS c +LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +WHERE o."OrderID" IS NOT NULL +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupJoin_GroupBy_Aggregate_2(bool async) + { + await base.GroupJoin_GroupBy_Aggregate_2(async); + + AssertSql( + """ +SELECT c."CustomerID" AS "Key", max(c."City") AS "Max" +FROM "Customers" AS c +LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +GROUP BY c."CustomerID" +"""); + } + + public override async Task GroupJoin_GroupBy_Aggregate_3(bool async) + { + await base.GroupJoin_GroupBy_Aggregate_3(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Average" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupJoin_GroupBy_Aggregate_4(bool async) + { + await base.GroupJoin_GroupBy_Aggregate_4(async); + + AssertSql( + """ +SELECT c."CustomerID" AS "Value", max(c."City") AS "Max" +FROM "Customers" AS c +LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +GROUP BY c."CustomerID" +"""); + } + + public override async Task GroupJoin_GroupBy_Aggregate_5(bool async) + { + await base.GroupJoin_GroupBy_Aggregate_5(async); + + AssertSql( + """ +SELECT o."OrderID" AS "Value", avg(o."OrderID"::double precision) AS "Average" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY o."OrderID" +"""); + } + + public override async Task GroupBy_optional_navigation_member_Aggregate(bool async) + { + await base.GroupBy_optional_navigation_member_Aggregate(async); + + AssertSql( + """ +SELECT c."Country", count(*)::int AS "Count" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY c."Country" +"""); + } + + public override async Task GroupJoin_complex_GroupBy_Aggregate(bool async) + { + await base.GroupJoin_complex_GroupBy_Aggregate(async); + + AssertSql( + """ +@p0='50' +@p='10' +@p1='100' + +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 @p0 OFFSET @p +) AS c0 +INNER JOIN ( + SELECT o."OrderID", o."CustomerID" + FROM "Orders" AS o + WHERE o."OrderID" < 10400 + ORDER BY o."OrderDate" NULLS FIRST + LIMIT @p1 +) AS o0 ON c0."CustomerID" = o0."CustomerID" +WHERE o0."OrderID" > 10300 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task Self_join_GroupBy_Aggregate(bool async) + { + await base.Self_join_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", avg(o0."OrderID"::double precision) AS "Count" +FROM "Orders" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +WHERE o."OrderID" < 10400 +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_multi_navigation_members_Aggregate(bool async) + { + await base.GroupBy_multi_navigation_members_Aggregate(async); + + AssertSql( + """ +SELECT o0."CustomerID", p."ProductName", count(*)::int AS "Count" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +INNER JOIN "Products" AS p ON o."ProductID" = p."ProductID" +GROUP BY o0."CustomerID", p."ProductName" +"""); + } + + public override async Task Union_simple_groupby(bool async) + { + await base.Union_simple_groupby(async); + + AssertSql( + """ +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 + WHERE c."ContactTitle" = 'Owner' + 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."City" = 'México D.F.' +) AS u +GROUP BY u."City" +"""); + } + + public override async Task Select_anonymous_GroupBy_Aggregate(bool async) + { + await base.Select_anonymous_GroupBy_Aggregate(async); + + AssertSql( + """ +SELECT min(o."OrderDate") AS "Min", max(o."OrderDate") AS "Max", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +WHERE o."OrderID" < 10300 +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_principal_key_property_optimization(bool async) + { + await base.GroupBy_principal_key_property_optimization(async); + + AssertSql( + """ +SELECT c."CustomerID" AS "Key", count(*)::int AS "Count" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY c."CustomerID" +"""); + } + + public override async Task GroupBy_after_anonymous_projection_and_distinct_followed_by_another_anonymous_projection(bool async) + { + await base.GroupBy_after_anonymous_projection_and_distinct_followed_by_another_anonymous_projection(async); + + AssertSql( + """ +SELECT o0."CustomerID" AS "Key", count(*)::int AS "Count" +FROM ( + SELECT DISTINCT o."CustomerID", o."OrderID" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task GroupBy_complex_key_aggregate(bool async) + { + await base.GroupBy_complex_key_aggregate(async); + + AssertSql( + """ +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 s +GROUP BY s."Key" +"""); + } + + public override async Task GroupBy_complex_key_aggregate_2(bool async) + { + await base.GroupBy_complex_key_aggregate_2(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."Key" +"""); + } + + public override async Task Select_collection_of_scalar_before_GroupBy_aggregate(bool async) + { + await base.Select_collection_of_scalar_before_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT c."City" AS "Key", count(*)::int AS "Count" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + #endregion GroupByAfterComposition + + #region GroupByAggregateComposition + + public override async Task GroupBy_OrderBy_key(bool async) + { + await base.GroupBy_OrderBy_key(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS c +FROM "Orders" AS o +GROUP BY o."CustomerID" +ORDER BY o."CustomerID" NULLS FIRST +"""); + } + + public override async Task GroupBy_OrderBy_count(bool async) + { + await base.GroupBy_OrderBy_count(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" +FROM "Orders" AS o +GROUP BY o."CustomerID" +ORDER BY count(*)::int NULLS FIRST, o."CustomerID" NULLS FIRST +"""); + } + + public override async Task GroupBy_OrderBy_count_Select_sum(bool async) + { + await base.GroupBy_OrderBy_count_Select_sum(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +ORDER BY count(*)::int NULLS FIRST, o."CustomerID" NULLS FIRST +"""); + } + + public override async Task GroupBy_aggregate_Contains(bool async) + { + await base.GroupBy_aggregate_Contains(async); + + AssertSql( + """ +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Orders" AS o +WHERE EXISTS ( + SELECT 1 + FROM "Orders" AS o0 + GROUP BY o0."CustomerID" + HAVING count(*)::int > 30 AND (o0."CustomerID" = o."CustomerID" OR (o0."CustomerID" IS NULL AND o."CustomerID" IS NULL))) +"""); + } + + public override async Task GroupBy_aggregate_Pushdown(bool async) + { + await base.GroupBy_aggregate_Pushdown(async); + + AssertSql( + """ +@p='20' +@p0='4' + +SELECT o0."CustomerID" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 10 + ORDER BY o."CustomerID" NULLS FIRST + LIMIT @p +) AS o0 +ORDER BY o0."CustomerID" NULLS FIRST +OFFSET @p0 +"""); + } + + public override async Task GroupBy_aggregate_using_grouping_key_Pushdown(bool async) + { + await base.GroupBy_aggregate_using_grouping_key_Pushdown(async); + + AssertSql( + """ +@p='20' +@p0='4' + +SELECT o0."Key", o0."Max" +FROM ( + SELECT o."CustomerID" AS "Key", max(o."CustomerID") AS "Max" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 10 + ORDER BY o."CustomerID" NULLS FIRST + LIMIT @p +) AS o0 +ORDER BY o0."Key" NULLS FIRST +OFFSET @p0 +"""); + } + + public override async Task GroupBy_aggregate_Pushdown_followed_by_projecting_Length(bool async) + { + await base.GroupBy_aggregate_Pushdown_followed_by_projecting_Length(async); + + AssertSql( + """ +@p='20' +@p0='4' + +SELECT length(o0."CustomerID")::int +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 10 + ORDER BY o."CustomerID" NULLS FIRST + LIMIT @p +) AS o0 +ORDER BY o0."CustomerID" NULLS FIRST +OFFSET @p0 +"""); + } + + public override async Task GroupBy_aggregate_Pushdown_followed_by_projecting_constant(bool async) + { + await base.GroupBy_aggregate_Pushdown_followed_by_projecting_constant(async); + + AssertSql( + """ +@p='20' +@p0='4' + +SELECT 5 +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 10 + ORDER BY o."CustomerID" NULLS FIRST + LIMIT @p +) AS o0 +ORDER BY o0."CustomerID" NULLS FIRST +OFFSET @p0 +"""); + } + + public override async Task GroupBy_filter_key(bool async) + { + await base.GroupBy_filter_key(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS c +FROM "Orders" AS o +GROUP BY o."CustomerID" +HAVING o."CustomerID" = 'ALFKI' +"""); + } + + public override async Task GroupBy_filter_count(bool async) + { + await base.GroupBy_filter_count(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" +FROM "Orders" AS o +GROUP BY o."CustomerID" +HAVING count(*)::int > 4 +"""); + } + + public override async Task GroupBy_count_filter(bool async) + { + await base.GroupBy_count_filter(async); + + AssertSql( + """ +SELECT o0."Key" AS "Name", count(*)::int AS "Count" +FROM ( + SELECT 'Order' AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +HAVING count(*)::int > 0 +"""); + } + + public override async Task GroupBy_filter_count_OrderBy_count_Select_sum(bool async) + { + await base.GroupBy_filter_count_OrderBy_count_Select_sum(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS "Count", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +HAVING count(*)::int > 4 +ORDER BY count(*)::int NULLS FIRST, o."CustomerID" NULLS FIRST +"""); + } + + public override async Task GroupBy_Aggregate_Join(bool async) + { + await base.GroupBy_Aggregate_Join(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", 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 o0 +INNER JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" +INNER JOIN "Orders" AS o1 ON o0."LastOrderID" = o1."OrderID" +"""); + } + + public override async Task GroupBy_Aggregate_Join_converted_from_SelectMany(bool async) + { + await base.GroupBy_Aggregate_Join_converted_from_SelectMany(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" +FROM "Customers" AS c +INNER JOIN ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 5 +) AS o0 ON c."CustomerID" = o0."CustomerID" +"""); + } + + public override async Task GroupBy_Aggregate_LeftJoin_converted_from_SelectMany(bool async) + { + await base.GroupBy_Aggregate_LeftJoin_converted_from_SelectMany(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" +FROM "Customers" AS c +LEFT JOIN ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 5 +) AS o0 ON c."CustomerID" = o0."CustomerID" +"""); + } + + public override async Task Join_GroupBy_Aggregate_multijoins(bool async) + { + await base.Join_GroupBy_Aggregate_multijoins(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", 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 o0 ON c."CustomerID" = o0."CustomerID" +INNER JOIN "Orders" AS o1 ON o0."LastOrderID" = o1."OrderID" +"""); + } + + public override async Task Join_GroupBy_Aggregate_single_join(bool async) + { + await base.Join_GroupBy_Aggregate_single_join(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."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 o0 ON c."CustomerID" = o0."CustomerID" +"""); + } + + public override async Task Join_GroupBy_Aggregate_with_another_join(bool async) + { + await base.Join_GroupBy_Aggregate_with_another_join(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."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 o0 ON c."CustomerID" = o0."CustomerID" +INNER JOIN "Orders" AS o1 ON c."CustomerID" = o1."CustomerID" +"""); + } + + public override async Task Join_GroupBy_Aggregate_distinct_single_join(bool async) + { + await base.Join_GroupBy_Aggregate_distinct_single_join(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", o1."LastOrderID" +FROM "Customers" AS c +INNER JOIN ( + 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 o0 + GROUP BY o0."CustomerID", o0."Year" + HAVING count(*)::int > 5 +) AS o1 ON c."CustomerID" = o1."CustomerID" +"""); + } + + public override async Task Join_GroupBy_Aggregate_with_left_join(bool async) + { + await base.Join_GroupBy_Aggregate_with_left_join(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."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 o0 ON c."CustomerID" = o0."CustomerID" +WHERE c."CustomerID" LIKE 'A%' +"""); + } + + public override async Task Join_GroupBy_Aggregate_in_subquery(bool async) + { + await base.Join_GroupBy_Aggregate_in_subquery(async); + + AssertSql( + """ +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" + FROM "Orders" AS o0 + GROUP BY o0."CustomerID" + HAVING count(*)::int > 5 + ) AS o1 ON c."CustomerID" = o1."CustomerID" +) AS s ON o."CustomerID" = s."CustomerID" +WHERE o."OrderID" < 10400 +"""); + } + + public override async Task Join_GroupBy_Aggregate_on_key(bool async) + { + await base.Join_GroupBy_Aggregate_on_key(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."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 o0 ON c."CustomerID" = o0."Key" +"""); + } + + public override async Task GroupBy_with_result_selector(bool async) + { + await base.GroupBy_with_result_selector(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Sum_constant(bool async) + { + await base.GroupBy_Sum_constant(async); + + AssertSql( + """ +SELECT COALESCE(sum(1), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Sum_constant_cast(bool async) + { + await base.GroupBy_Sum_constant_cast(async); + + AssertSql( + """ +SELECT COALESCE(sum(1), 0.0)::bigint +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task Distinct_GroupBy_OrderBy_key(bool async) + { + await base.Distinct_GroupBy_OrderBy_key(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."CustomerID" +ORDER BY o0."CustomerID" NULLS FIRST +"""); + } + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/28410")] + public override async Task Select_nested_collection_with_groupby(bool async) + { + await base.Select_nested_collection_with_groupby(async); + + AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Orders" AS o + WHERE c."CustomerID" = o."CustomerID"), c."CustomerID", t."OrderID" +FROM "Customers" AS c +LEFT JOIN LATERAL ( + SELECT o0."OrderID" + FROM "Orders" AS o0 + WHERE c."CustomerID" = o0."CustomerID" + GROUP BY o0."OrderID" +) AS t ON TRUE +WHERE c."CustomerID" LIKE 'F%' +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Select_uncorrelated_collection_with_groupby_works(bool async) + { + await base.Select_uncorrelated_collection_with_groupby_works(async); + + AssertSql( + """ +SELECT c."CustomerID", o0."OrderID" +FROM "Customers" AS c +LEFT JOIN LATERAL ( + SELECT o."OrderID" + FROM "Orders" AS o + GROUP BY o."OrderID" +) AS o0 ON TRUE +WHERE c."CustomerID" LIKE 'A%' +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Select_uncorrelated_collection_with_groupby_multiple_collections_work(bool async) + { + await base.Select_uncorrelated_collection_with_groupby_multiple_collections_work(async); + + AssertSql( + """ +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 p1 ON TRUE +LEFT JOIN LATERAL ( + SELECT count(*)::int AS c, p0."ProductID" + FROM "Products" AS p0 + GROUP BY p0."ProductID" +) AS p2 ON TRUE +WHERE o."CustomerID" LIKE 'A%' +ORDER BY o."OrderID" NULLS FIRST, p1."ProductID" NULLS FIRST +"""); + } + + public override async Task Select_GroupBy_All(bool async) + { + await base.Select_GroupBy_All(async); + + AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING o."CustomerID" <> 'ALFKI' OR o."CustomerID" IS NULL) +"""); + } + + #endregion GroupByAggregateComposition + + #region GroupByAggregateChainComposition + + public override async Task GroupBy_Where_Average(bool async) + { + await base.GroupBy_Where_Average(async); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision) FILTER (WHERE o."OrderID" < 10300) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Count(bool async) + { + await base.GroupBy_Where_Count(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_LongCount(bool async) + { + await base.GroupBy_Where_LongCount(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Max(bool async) + { + await base.GroupBy_Where_Max(async); + + AssertSql( + """ +SELECT max(o."OrderID") FILTER (WHERE o."OrderID" < 10300) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Min(bool async) + { + await base.GroupBy_Where_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") FILTER (WHERE o."OrderID" < 10300) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Sum(bool async) + { + await base.GroupBy_Where_Sum(async); + + AssertSql( + """ +SELECT COALESCE(sum(o."OrderID") FILTER (WHERE o."OrderID" < 10300), 0)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Count_with_predicate(bool async) + { + await base.GroupBy_Where_Count_with_predicate(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Where_Count(bool async) + { + await base.GroupBy_Where_Where_Count(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Select_Where_Count(bool async) + { + await base.GroupBy_Where_Select_Where_Count(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Where_Select_Where_Select_Min(bool async) + { + await base.GroupBy_Where_Select_Where_Select_Min(async); + + AssertSql( + """ +SELECT min(o."OrderID") FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_multiple_Count_with_predicate(bool async) + { + await base.GroupBy_multiple_Count_with_predicate(async); + + AssertSql( + """ +SELECT o."CustomerID", count(*)::int AS "All", count(*) FILTER (WHERE o."OrderID" < 11000)::int AS "TenK", count(*) FILTER (WHERE o."OrderID" < 12000)::int AS "EleventK" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_multiple_Sum_with_conditional_projection(bool async) + { + await base.GroupBy_multiple_Sum_with_conditional_projection(async); + + AssertSql( + """ +SELECT o."CustomerID", COALESCE(sum(CASE + WHEN o."OrderID" < 11000 THEN o."OrderID" + ELSE 0 +END), 0)::int AS "TenK", COALESCE(sum(CASE + WHEN o."OrderID" >= 11000 THEN o."OrderID" + ELSE 0 +END), 0)::int AS "EleventK" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_multiple_Sum_with_Select_conditional_projection(bool async) + { + await base.GroupBy_multiple_Sum_with_Select_conditional_projection(async); + + AssertSql( + """ +SELECT o."CustomerID", COALESCE(sum(CASE + WHEN o."OrderID" < 11000 THEN o."OrderID" + ELSE 0 +END), 0)::int AS "TenK", COALESCE(sum(CASE + WHEN o."OrderID" >= 11000 THEN o."OrderID" + ELSE 0 +END), 0)::int AS "EleventK" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Key_as_part_of_element_selector(bool async) + { + await base.GroupBy_Key_as_part_of_element_selector(async); + + AssertSql( + """ +SELECT o."OrderID" AS "Key", avg(o."OrderID"::double precision) AS "Avg", max(o."OrderDate") AS "Max" +FROM "Orders" AS o +GROUP BY o."OrderID" +"""); + } + + public override async Task GroupBy_composite_Key_as_part_of_element_selector(bool async) + { + await base.GroupBy_composite_Key_as_part_of_element_selector(async); + + AssertSql( + """ +SELECT o."OrderID", o."CustomerID", avg(o."OrderID"::double precision) AS "Avg", max(o."OrderDate") AS "Max" +FROM "Orders" AS o +GROUP BY o."OrderID", o."CustomerID" +"""); + } + + public override async Task GroupBy_with_aggregate_through_navigation_property(bool async) + { + await base.GroupBy_with_aggregate_through_navigation_property(async); + + AssertSql( + """ +SELECT ( + SELECT max(c."Region") + FROM "Orders" AS o0 + LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" + WHERE o."EmployeeID" = o0."EmployeeID" OR (o."EmployeeID" IS NULL AND o0."EmployeeID" IS NULL)) AS max +FROM "Orders" AS o +GROUP BY o."EmployeeID" +"""); + } + + public override async Task GroupBy_with_aggregate_containing_complex_where(bool async) + { + await base.GroupBy_with_aggregate_containing_complex_where(async); + + AssertSql( + """ +SELECT o."EmployeeID" AS "Key", ( + SELECT max(o0."OrderID") + FROM "Orders" AS o0 + WHERE o0."EmployeeID"::bigint = CAST(max(o."OrderID") * 6 AS bigint) OR (o0."EmployeeID" IS NULL AND max(o."OrderID") IS NULL)) AS "Max" +FROM "Orders" AS o +GROUP BY o."EmployeeID" +"""); + } + + #endregion GroupByWithoutAggregate + + #region GroupBySelectFirst + + public override async Task GroupBy_Shadow(bool async) + { + await base.GroupBy_Shadow(async); + + AssertSql( + """ +SELECT ( + SELECT e0."Title" + FROM "Employees" AS e0 + WHERE e0."Title" = 'Sales Representative' AND e0."EmployeeID" = 1 AND (e."Title" = e0."Title" OR (e."Title" IS NULL AND e0."Title" IS NULL)) + LIMIT 1) +FROM "Employees" AS e +WHERE e."Title" = 'Sales Representative' AND e."EmployeeID" = 1 +GROUP BY e."Title" +"""); + } + + public override async Task GroupBy_Shadow2(bool async) + { + await base.GroupBy_Shadow2(async); + + AssertSql( + """ +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 e1 +LEFT JOIN ( + 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 e2 + WHERE e2.row <= 1 +) AS e3 ON e1."Title" = e3."Title" +"""); + } + + public override async Task GroupBy_Shadow3(bool async) + { + await base.GroupBy_Shadow3(async); + + AssertSql( + """ +SELECT ( + SELECT e0."Title" + FROM "Employees" AS e0 + WHERE e0."EmployeeID" = 1 AND e."EmployeeID" = e0."EmployeeID" + LIMIT 1) +FROM "Employees" AS e +WHERE e."EmployeeID" = 1 +GROUP BY e."EmployeeID" +"""); + } + + public override async Task GroupBy_select_grouping_list(bool async) + { + await base.GroupBy_select_grouping_list(async); + + AssertSql( + """ +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 c1 +LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" +ORDER BY c1."City" NULLS FIRST +"""); + } + + public override async Task GroupBy_select_grouping_array(bool async) + { + await base.GroupBy_select_grouping_array(async); + + AssertSql( + """ +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 c1 +LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" +ORDER BY c1."City" NULLS FIRST +"""); + } + + public override async Task GroupBy_select_grouping_composed_list(bool async) + { + await base.GroupBy_select_grouping_composed_list(async); + + AssertSql( + """ +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 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 c2 ON c1."City" = c2."City" +ORDER BY c1."City" NULLS FIRST +"""); + } + + public override async Task GroupBy_select_grouping_composed_list_2(bool async) + { + await base.GroupBy_select_grouping_composed_list_2(async); + + AssertSql( + """ +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 c1 +LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" +ORDER BY c1."City" NULLS FIRST, c0."CustomerID" NULLS FIRST +"""); + } + + public override async Task Select_GroupBy_SelectMany(bool async) + { + await base.Select_GroupBy_SelectMany(async); + + AssertSql(); + } + + #endregion GroupByEntityType + + #region ResultOperatorsAfterGroupBy + + public override async Task Count_after_GroupBy_aggregate(bool async) + { + await base.Count_after_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT count(*)::int +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +"""); + } + + public override async Task LongCount_after_GroupBy_aggregate(bool async) + { + await base.LongCount_after_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT count(*) +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +"""); + } + + public override async Task GroupBy_Select_Distinct_aggregate(bool async) + { + await base.GroupBy_Select_Distinct_aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", avg(DISTINCT o."OrderID"::double precision) AS "Average", count(DISTINCT o."EmployeeID")::int AS "Count", count(DISTINCT o."EmployeeID") AS "LongCount", max(o."OrderDate") AS "Max", min(o."OrderDate") AS "Min", COALESCE(sum(DISTINCT o."OrderID"), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_group_Distinct_Select_Distinct_aggregate(bool async) + { + await base.GroupBy_group_Distinct_Select_Distinct_aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", max(o."OrderDate") AS "Max" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_group_Where_Select_Distinct_aggregate(bool async) + { + await base.GroupBy_group_Where_Select_Distinct_aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", max(o."OrderDate") FILTER (WHERE o."OrderDate" IS NOT NULL) AS "Max" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task MinMax_after_GroupBy_aggregate(bool async) + { + await base.MinMax_after_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT min(o0.c) +FROM ( + SELECT COALESCE(sum(o."OrderID"), 0)::int AS c + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +""", + // + """ +SELECT max(o0.c) +FROM ( + SELECT COALESCE(sum(o."OrderID"), 0)::int AS c + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +"""); + } + + public override async Task All_after_GroupBy_aggregate(bool async) + { + await base.All_after_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING FALSE) +"""); + } + + public override async Task All_after_GroupBy_aggregate2(bool async) + { + await base.All_after_GroupBy_aggregate2(async); + + AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING COALESCE(sum(o."OrderID"), 0)::int < 0) +"""); + } + + public override async Task Any_after_GroupBy_aggregate(bool async) + { + await base.Any_after_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID") +"""); + } + + public override async Task Count_after_GroupBy_without_aggregate(bool async) + { + await base.Count_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT count(*)::int +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +"""); + } + + public override async Task Count_with_predicate_after_GroupBy_without_aggregate(bool async) + { + await base.Count_with_predicate_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT count(*)::int +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 1 +) AS o0 +"""); + } + + public override async Task LongCount_after_GroupBy_without_aggregate(bool async) + { + await base.LongCount_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT count(*) +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +"""); + } + + public override async Task LongCount_with_predicate_after_GroupBy_without_aggregate(bool async) + { + await base.LongCount_with_predicate_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT count(*) +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 1 +) AS o0 +"""); + } + + public override async Task Any_after_GroupBy_without_aggregate(bool async) + { + await base.Any_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID") +"""); + } + + public override async Task Any_with_predicate_after_GroupBy_without_aggregate(bool async) + { + await base.Any_with_predicate_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int > 1) +"""); + } + + public override async Task All_with_predicate_after_GroupBy_without_aggregate(bool async) + { + await base.All_with_predicate_after_GroupBy_without_aggregate(async); + + AssertSql( + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" + HAVING count(*)::int <= 1) +"""); + } + + public override async Task GroupBy_aggregate_followed_by_another_GroupBy_aggregate(bool async) + { + await base.GroupBy_aggregate_followed_by_another_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT o1."Key0" AS "Key", COALESCE(sum(o1."Count"), 0)::int AS "Count" +FROM ( + SELECT o0."Count", 1 AS "Key0" + FROM ( + SELECT count(*)::int AS "Count" + FROM "Orders" AS o + GROUP BY o."CustomerID" + ) AS o0 +) AS o1 +GROUP BY o1."Key0" +"""); + } + + public override async Task GroupBy_Count_in_projection(bool async) + { + await base.GroupBy_Count_in_projection(async); + + AssertSql( + """ +SELECT o."OrderID", o."OrderDate", EXISTS ( + SELECT 1 + FROM "Order Details" AS o0 + 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 +"""); + } + + public override async Task GroupBy_nominal_type_count(bool async) + { + await base.GroupBy_nominal_type_count(async); + + AssertSql( + """ +SELECT count(*)::int +FROM ( + SELECT 1 + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +"""); + } + + public override async Task GroupBy_based_on_renamed_property_simple(bool async) + { + await base.GroupBy_based_on_renamed_property_simple(async); + + AssertSql( + """ +SELECT c."City" AS "Renamed", count(*)::int AS "Count" +FROM "Customers" AS c +GROUP BY c."City" +"""); + } + + public override async Task GroupBy_based_on_renamed_property_complex(bool async) + { + await base.GroupBy_based_on_renamed_property_complex(async); + + AssertSql( + """ +SELECT c0."Renamed" AS "Key", count(*)::int AS "Count" +FROM ( + SELECT DISTINCT c."City" AS "Renamed", c."CustomerID" + FROM "Customers" AS c +) AS c0 +GROUP BY c0."Renamed" +"""); + } + + public override async Task Join_groupby_anonymous_orderby_anonymous_projection(bool async) + { + await base.Join_groupby_anonymous_orderby_anonymous_projection(async); + + AssertSql( + """ +SELECT c."CustomerID", o."OrderDate" +FROM "Customers" AS c +INNER JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +GROUP BY c."CustomerID", o."OrderDate" +ORDER BY o."OrderDate" NULLS FIRST +"""); + } + + public override async Task Odata_groupby_empty_key(bool async) + { + await base.Odata_groupby_empty_key(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_with_group_key_access_thru_navigation(bool async) + { + await base.GroupBy_with_group_key_access_thru_navigation(async); + + AssertSql( + """ +SELECT o0."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +GROUP BY o0."CustomerID" +"""); + } + + public override async Task GroupBy_with_group_key_access_thru_nested_navigation(bool async) + { + await base.GroupBy_with_group_key_access_thru_nested_navigation(async); + + AssertSql( + """ +SELECT c."Country" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" +GROUP BY c."Country" +"""); + } + + #endregion GroupBySelectFirst + + #region GroupByEntityType + + public override async Task GroupBy_with_group_key_being_navigation(bool async) + { + await base.GroupBy_with_group_key_being_navigation(async); + + AssertSql( + """ +SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +GROUP BY o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +"""); + } + + public override async Task GroupBy_with_group_key_being_nested_navigation(bool async) + { + await base.GroupBy_with_group_key_being_nested_navigation(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", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" +GROUP BY c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +"""); + } + + public override async Task GroupBy_with_group_key_being_navigation_with_entity_key_projection(bool async) + { + await base.GroupBy_with_group_key_being_navigation_with_entity_key_projection(async); + + AssertSql( + """ +SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +FROM "Order Details" AS o +INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" +GROUP BY o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +"""); + } + + public override async Task GroupBy_with_group_key_being_navigation_with_complex_projection(bool async) + { + await base.GroupBy_with_group_key_being_navigation_with_complex_projection(async); + + AssertSql(); + } + + public override async Task GroupBy_with_order_by_skip_and_another_order_by(bool async) + { + await base.GroupBy_with_order_by_skip_and_another_order_by(async); + + AssertSql( + """ +@p='80' + +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 +) AS o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_Count_with_predicate(bool async) + { + await base.GroupBy_Property_Select_Count_with_predicate(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300)::int +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_Property_Select_LongCount_with_predicate(bool async) + { + await base.GroupBy_Property_Select_LongCount_with_predicate(async); + + AssertSql( + """ +SELECT count(*) FILTER (WHERE o."OrderID" < 10300) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_orderby_projection_with_coalesce_operation(bool async) + { + await base.GroupBy_orderby_projection_with_coalesce_operation(async); + + AssertSql( + """ +SELECT COALESCE(c."City", 'Unknown') AS "Locality", count(*)::int AS "Count" +FROM "Customers" AS c +GROUP BY c."City" +ORDER BY count(*)::int DESC NULLS LAST, c."City" NULLS FIRST +"""); + } + + public override async Task GroupBy_let_orderby_projection_with_coalesce_operation(bool async) + { + await base.GroupBy_let_orderby_projection_with_coalesce_operation(async); + + AssertSql(); + } + + public override async Task GroupBy_Min_Where_optional_relationship(bool async) + { + await base.GroupBy_Min_Where_optional_relationship(async); + + AssertSql( + """ +SELECT c."CustomerID" AS "Key", count(*)::int AS "Count" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY c."CustomerID" +HAVING count(*)::int <> 2 +"""); + } + + public override async Task GroupBy_Min_Where_optional_relationship_2(bool async) + { + await base.GroupBy_Min_Where_optional_relationship_2(async); + + AssertSql( + """ +SELECT c."CustomerID" AS "Key", count(*)::int AS "Count" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY c."CustomerID" +HAVING count(*)::int < 2 OR count(*)::int > 2 +"""); + } + + public override async Task GroupBy_aggregate_over_a_subquery(bool async) + { + await base.GroupBy_aggregate_over_a_subquery(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", ( + SELECT count(*)::int + FROM "Customers" AS c + WHERE c."CustomerID" = o."CustomerID") AS "Count" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_aggregate_join_with_grouping_key(bool async) + { + await base.GroupBy_aggregate_join_with_grouping_key(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."Count" +FROM ( + SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o0 +INNER JOIN "Customers" AS c ON o0."Key" = c."CustomerID" +"""); + } + + public override async Task GroupBy_aggregate_join_with_group_result(bool async) + { + await base.GroupBy_aggregate_join_with_group_result(async); + + AssertSql( + """ +SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +FROM ( + SELECT o."CustomerID" AS "Key", max(o."OrderDate") AS "LastOrderDate" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) 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)) +"""); + } + + public override async Task GroupBy_aggregate_from_right_side_of_join(bool async) + { + await base.GroupBy_aggregate_from_right_side_of_join(async); + + AssertSql( + """ +@p='10' + +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 o0 ON c."CustomerID" = o0."Key" +ORDER BY o0."Max" NULLS FIRST, c."CustomerID" NULLS FIRST +LIMIT @p OFFSET @p +"""); + } + + public override async Task GroupBy_aggregate_join_another_GroupBy_aggregate(bool async) + { + await base.GroupBy_aggregate_join_another_GroupBy_aggregate(async); + + AssertSql( + """ +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 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 o2 ON o1."Key" = o2."Key" +"""); + } + + public override async Task GroupBy_aggregate_after_skip_0_take_0(bool async) + { + await base.GroupBy_aggregate_after_skip_0_take_0(async); + + AssertSql( + """ +@p='0' + +SELECT o0."CustomerID" AS "Key", count(*)::int AS "Total" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + LIMIT @p OFFSET @p +) AS o0 +GROUP BY o0."CustomerID" +"""); + } + + public override async Task GroupBy_skip_0_take_0_aggregate(bool async) + { + await base.GroupBy_skip_0_take_0_aggregate(async); + + AssertSql( + """ +@p='0' + +SELECT o."CustomerID" AS "Key", count(*)::int AS "Total" +FROM "Orders" AS o +WHERE o."OrderID" > 10500 +GROUP BY o."CustomerID" +LIMIT @p OFFSET @p +"""); + } + + public override async Task GroupBy_aggregate_followed_another_GroupBy_aggregate(bool async) + { + await base.GroupBy_aggregate_followed_another_GroupBy_aggregate(async); + + AssertSql( + """ +SELECT o1."CustomerID" AS "Key", count(*)::int AS "Count" +FROM ( + SELECT o0."CustomerID" + FROM ( + SELECT o."CustomerID", date_part('year', o."OrderDate")::int AS "Year" + FROM "Orders" AS o + ) AS o0 + GROUP BY o0."CustomerID", o0."Year" +) AS o1 +GROUP BY o1."CustomerID" +"""); + } + + public override async Task GroupBy_aggregate_without_selectMany_selecting_first(bool async) + { + await base.GroupBy_aggregate_without_selectMany_selecting_first(async); + + AssertSql( + """ +SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" +FROM ( + SELECT min(o."OrderID") AS c + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o1 +CROSS JOIN "Orders" AS o0 +WHERE o0."OrderID" = o1.c +"""); + } + + public override async Task GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(bool async) + { + await base.GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(async); + + AssertSql( + """ +SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT MIN([o].[OrderID]) AS [c] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] +) AS [t] +CROSS JOIN [Orders] AS [o0] +WHERE [o0].[OrderID] = [t].[c] +"""); + } + + public override async Task GroupBy_selecting_grouping_key_list(bool async) + { + await base.GroupBy_selecting_grouping_key_list(async); + + AssertSql( + """ +SELECT o1."CustomerID", o0."CustomerID", o0."OrderID" +FROM ( + SELECT o."CustomerID" + FROM "Orders" AS o + GROUP BY o."CustomerID" +) AS o1 +LEFT JOIN "Orders" AS o0 ON o1."CustomerID" = o0."CustomerID" +ORDER BY o1."CustomerID" NULLS FIRST +"""); + } + + public override async Task GroupBy_with_grouping_key_using_Like(bool async) + { + await base.GroupBy_with_grouping_key_using_Like(async); + + AssertSql( + """ +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 o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_with_grouping_key_DateTime_Day(bool async) + { + await base.GroupBy_with_grouping_key_DateTime_Day(async); + + AssertSql( + """ +SELECT o0."Key", count(*)::int AS "Count" +FROM ( + SELECT date_part('day', o."OrderDate")::int AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task GroupBy_with_cast_inside_grouping_aggregate(bool async) + { + await base.GroupBy_with_cast_inside_grouping_aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", count(*)::int AS "Count", COALESCE(sum(o."OrderID"::bigint), 0.0)::bigint AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + #endregion ResultOperatorsAfterGroupBy + + #region GroupByInSubquery + + public override async Task Complex_query_with_groupBy_in_subquery1(bool async) + { + await base.Complex_query_with_groupBy_in_subquery1(async); + + AssertSql( + """ +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 o0 ON TRUE +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Complex_query_with_groupBy_in_subquery2(bool async) + { + await base.Complex_query_with_groupBy_in_subquery2(async); + + AssertSql( + """ +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 o0 ON TRUE +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Complex_query_with_groupBy_in_subquery3(bool async) + { + await base.Complex_query_with_groupBy_in_subquery3(async); + + AssertSql( + """ +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 o0 ON TRUE +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Group_by_with_projection_into_DTO(bool async) + { + await base.Group_by_with_projection_into_DTO(async); + + AssertSql( + """ +SELECT o."OrderID"::bigint AS "Id", count(*)::int AS "Count" +FROM "Orders" AS o +GROUP BY o."OrderID" +"""); + } + + public override async Task Where_select_function_groupby_followed_by_another_select_with_aggregates(bool async) + { + await base.Where_select_function_groupby_followed_by_another_select_with_aggregates(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(sum(CASE + WHEN 2020 - date_part('year', o."OrderDate")::int <= 30 THEN o."OrderID" + ELSE 0 +END), 0)::int AS "Sum1", COALESCE(sum(CASE + WHEN 2020 - date_part('year', o."OrderDate")::int > 30 AND 2020 - date_part('year', o."OrderDate")::int <= 60 THEN o."OrderID" + ELSE 0 +END), 0)::int AS "Sum2" +FROM "Orders" AS o +WHERE o."CustomerID" LIKE 'A%' +GROUP BY o."CustomerID" +"""); + } + + public override async Task Group_by_column_project_constant(bool async) + { + await base.Group_by_column_project_constant(async); + + AssertSql( + """ +SELECT 42 +FROM "Orders" AS o +GROUP BY o."CustomerID" +ORDER BY o."CustomerID" NULLS FIRST +"""); + } + + public override async Task Key_plus_key_in_projection(bool async) + { + await base.Key_plus_key_in_projection(async); + + AssertSql( + """ +SELECT o."OrderID" + o."OrderID" AS "Value", avg(o."OrderID"::double precision) AS "Average" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +GROUP BY o."OrderID" +"""); + } + + public override async Task Group_by_with_arithmetic_operation_inside_aggregate(bool async) + { + await base.Group_by_with_arithmetic_operation_inside_aggregate(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID" + length(o."CustomerID")::int), 0)::int AS "Sum" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_scalar_subquery(bool async) + { + await base.GroupBy_scalar_subquery(async); + + AssertSql( + """ +SELECT o0."Key", count(*)::int AS "Count" +FROM ( + SELECT ( + SELECT c."ContactName" + FROM "Customers" AS c + WHERE c."CustomerID" = o."CustomerID" + LIMIT 1) AS "Key" + FROM "Orders" AS o +) AS o0 +GROUP BY o0."Key" +"""); + } + + public override async Task AsEnumerable_in_subquery_for_GroupBy(bool async) + { + await base.AsEnumerable_in_subquery_for_GroupBy(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", s."OrderID", s."CustomerID", s."EmployeeID", s."OrderDate", s."CustomerID0" +FROM "Customers" AS c +LEFT JOIN LATERAL ( + 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 o1 + LEFT JOIN ( + 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 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, s."CustomerID0" NULLS FIRST +"""); + } + + public override async Task GroupBy_aggregate_from_multiple_query_in_same_projection(bool async) + { + await base.GroupBy_aggregate_from_multiple_query_in_same_projection(async); + + AssertSql( + """ +SELECT [t].[CustomerID], [t0].[Key], [t0].[C], [t0].[c0] +FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] +) AS [t] +OUTER APPLY ( + SELECT TOP(1) [e].[City] AS [Key], COUNT(*) + ( + SELECT COUNT(*) + FROM [Orders] AS [o0] + WHERE [t].[CustomerID] = [o0].[CustomerID] OR ([t].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) AS [C], 1 AS [c0] + FROM [Employees] AS [e] + WHERE [e].[City] = N'Seattle' + GROUP BY [e].[City] + ORDER BY (SELECT 1) +) AS [t0] +"""); + } + + public override async Task GroupBy_aggregate_from_multiple_query_in_same_projection_2(bool async) + { + await base.GroupBy_aggregate_from_multiple_query_in_same_projection_2(async); + + AssertSql( + """ +SELECT o."CustomerID" AS "Key", COALESCE(( + SELECT count(*)::int + min(o."OrderID") + FROM "Employees" AS e + WHERE e."City" = 'Seattle' + GROUP BY e."City" + ORDER BY (SELECT 1) NULLS FIRST + LIMIT 1), 0) AS "A" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + public override async Task GroupBy_aggregate_from_multiple_query_in_same_projection_3(bool async) + { + await base.GroupBy_aggregate_from_multiple_query_in_same_projection_3(async); + + AssertSql( + """ +SELECT [o].[CustomerID] AS [Key], COALESCE(( + SELECT TOP(1) COUNT(*) + ( + SELECT COUNT(*) + FROM [Orders] AS [o0] + WHERE [o].[CustomerID] = [o0].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) + FROM [Employees] AS [e] + WHERE [e].[City] = N'Seattle' + GROUP BY [e].[City] + ORDER BY COUNT(*) + ( + SELECT COUNT(*) + FROM [Orders] AS [o0] + WHERE [o].[CustomerID] = [o0].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL))), 0) AS [A] +FROM [Orders] AS [o] +GROUP BY [o].[CustomerID] +"""); + } + + public override async Task GroupBy_scalar_aggregate_in_set_operation(bool async) + { + await base.GroupBy_scalar_aggregate_in_set_operation(async); + + AssertSql( + """ +SELECT c."CustomerID", 0 AS "Sequence" +FROM "Customers" AS c +WHERE c."CustomerID" LIKE 'F%' +UNION +SELECT o."CustomerID", 1 AS "Sequence" +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + #endregion GroupByInSubquery + + #region GroupByAndDistinctWithCorrelatedCollection + + public override async Task Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(bool async) + { + await base.Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(async); + + AssertSql( + """ +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 s +LEFT JOIN LATERAL ( + SELECT p."ProductID" + FROM "Products" AS p + GROUP BY p."ProductID" +) AS p1 ON TRUE +LEFT JOIN LATERAL ( + SELECT count(*)::int AS c, p0."ProductID" + FROM "Products" AS p0 + GROUP BY p0."ProductID" +) AS p2 ON TRUE +ORDER BY s."City" NULLS FIRST, p1."ProductID" NULLS FIRST +"""); + } + + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(bool async) + { + await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(async); + + AssertSql( + """ +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 c0 +LEFT JOIN "Orders" AS o ON c0."CustomerID" = o."CustomerID" +ORDER BY c0."CustomerID" NULLS FIRST +"""); + } + + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(bool async) + { + await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(async); + + AssertSql( + """ +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 o1 +LEFT JOIN "Orders" AS o0 ON o1."CustomerID" = o0."CustomerID" +ORDER BY o1."CustomerID" NULLS FIRST +"""); + } + + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) + => await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(async); + + //AssertSql(" "); + public override async Task Complex_query_with_group_by_in_subquery5(bool async) + { + await base.Complex_query_with_group_by_in_subquery5(async); + + AssertSql( + """ +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 + INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" + LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" + WHERE c."CustomerID" = 'ALFKI' + GROUP BY o."ProductID" +) AS s +LEFT JOIN LATERAL ( + SELECT c0."CustomerID", c0."City" + FROM "Customers" AS c0 + WHERE length(c0."CustomerID")::int < s.c0 +) AS c1 ON TRUE +ORDER BY s."ProductID" NULLS FIRST, c1."CustomerID" NULLS FIRST +"""); + } + + public override async Task Complex_query_with_groupBy_in_subquery4(bool async) + { + await base.Complex_query_with_groupBy_in_subquery4(async); + + AssertSql( + """ +SELECT c."CustomerID", s1."Sum", s1."Count", s1."Key" +FROM "Customers" AS c +LEFT JOIN LATERAL ( + SELECT COALESCE(sum(s."OrderID"), 0)::int AS "Sum", ( + SELECT count(*)::int + FROM ( + 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 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(c0."City", '') || COALESCE(o."CustomerID", '') AS "Key" + FROM "Orders" AS o + LEFT JOIN "Customers" AS c0 ON o."CustomerID" = c0."CustomerID" + WHERE c."CustomerID" = o."CustomerID" + ) AS s + GROUP BY s."Key" +) AS s1 ON TRUE +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + public override async Task GroupBy_aggregate_SelectMany(bool async) + { + await base.GroupBy_aggregate_SelectMany(async); + + AssertSql(); + } + + public override async Task Final_GroupBy_property_entity(bool async) + { + await base.Final_GroupBy_property_entity(async); + + AssertSql( + """ +SELECT c."City", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +ORDER BY c."City" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_entity(bool async) + { + await base.Final_GroupBy_entity(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", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Orders" AS o +LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" +WHERE o."OrderID" < 10500 +ORDER BY c."CustomerID" NULLS FIRST, c."Address" NULLS FIRST, c."City" NULLS FIRST, c."CompanyName" NULLS FIRST, c."ContactName" NULLS FIRST, c."ContactTitle" NULLS FIRST, c."Country" NULLS FIRST, c."Fax" NULLS FIRST, c."Phone" NULLS FIRST, c."PostalCode" NULLS FIRST, c."Region" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_entity_non_nullable(bool async) + { + await base.Final_GroupBy_property_entity_non_nullable(async); + + AssertSql( + """ +SELECT o."OrderID", o."ProductID", o."Discount", o."Quantity", o."UnitPrice" +FROM "Order Details" AS o +WHERE o."OrderID" < 10500 +ORDER BY o."OrderID" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_anonymous_type(bool async) + { + await base.Final_GroupBy_property_anonymous_type(async); + + AssertSql( + """ +SELECT c."City", c."ContactName", c."ContactTitle" +FROM "Customers" AS c +ORDER BY c."City" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_multiple_properties_entity(bool async) + { + await base.Final_GroupBy_multiple_properties_entity(async); + + AssertSql( + """ +SELECT c."City", c."Region", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode" +FROM "Customers" AS c +ORDER BY c."City" NULLS FIRST, c."Region" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_complex_key_entity(bool async) + { + await base.Final_GroupBy_complex_key_entity(async); + + AssertSql( + """ +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 c0 +ORDER BY c0."City" NULLS FIRST, c0."Region" NULLS FIRST, c0."Constant" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_nominal_type_entity(bool async) + { + await base.Final_GroupBy_nominal_type_entity(async); + + AssertSql( + """ +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 c0 +ORDER BY c0."City" NULLS FIRST, c0."Constant" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_anonymous_type_element_selector(bool async) + { + await base.Final_GroupBy_property_anonymous_type_element_selector(async); + + AssertSql( + """ +SELECT c."City", c."ContactName", c."ContactTitle" +FROM "Customers" AS c +ORDER BY c."City" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_entity_Include_collection(bool async) + { + await base.Final_GroupBy_property_entity_Include_collection(async); + + AssertSql( + """ +SELECT c."City", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Customers" AS c +LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +WHERE c."Country" = 'USA' +ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_entity_projecting_collection(bool async) + { + await base.Final_GroupBy_property_entity_projecting_collection(async); + + AssertSql( + """ +SELECT c."City", c."CustomerID", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Customers" AS c +LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" +WHERE c."Country" = 'USA' +ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_entity_projecting_collection_composed(bool async) + { + await base.Final_GroupBy_property_entity_projecting_collection_composed(async); + + AssertSql( + """ +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 o0 ON c."CustomerID" = o0."CustomerID" +WHERE c."Country" = 'USA' +ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST +"""); + } + + public override async Task Final_GroupBy_property_entity_projecting_collection_and_single_result(bool async) + { + await base.Final_GroupBy_property_entity_projecting_collection_and_single_result(async); + + AssertSql( + """ +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 o1 ON c."CustomerID" = o1."CustomerID" +LEFT JOIN ( + 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 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 +"""); + } + + public override async Task Final_GroupBy_TagWith(bool async) + { + await base.Final_GroupBy_TagWith(async); + + AssertSql( + """ +-- foo + +SELECT c."City", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" +FROM "Customers" AS c +ORDER BY c."City" NULLS FIRST +"""); + } + + #endregion Final + + #region GroupByWithoutAggregate + + public override async Task GroupBy_Where_with_grouping_result(bool async) + { + await base.GroupBy_Where_with_grouping_result(async); + + AssertSql(); + } + + public override async Task GroupBy_OrderBy_with_grouping_result(bool async) + { + await base.GroupBy_OrderBy_with_grouping_result(async); + + AssertSql(); + } + + public override async Task GroupBy_SelectMany(bool async) + { + await base.GroupBy_SelectMany(async); + + AssertSql(); + } + + public override async Task OrderBy_GroupBy_SelectMany(bool async) + { + await base.OrderBy_GroupBy_SelectMany(async); + + AssertSql(); + } + + public override async Task OrderBy_GroupBy_SelectMany_shadow(bool async) + { + await base.OrderBy_GroupBy_SelectMany_shadow(async); + + AssertSql(); + } + + public override async Task GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(bool async) + { + await base.GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(async); + + AssertSql(); + } + + public override async Task GroupBy_Distinct(bool async) + { + await base.GroupBy_Distinct(async); + + AssertSql(); + } + + public override async Task GroupBy_complex_key_without_aggregate(bool async) + { + await base.GroupBy_complex_key_without_aggregate(async); + + AssertSql( + """ +SELECT s1."Key", s3."OrderID", s3."CustomerID", s3."EmployeeID", s3."OrderDate", s3."CustomerID0" +FROM ( + 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 s + GROUP BY s."Key" +) AS s1 +LEFT JOIN ( + SELECT s2."OrderID", s2."CustomerID", s2."EmployeeID", s2."OrderDate", s2."CustomerID0", s2."Key" + FROM ( + 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 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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task GroupBy_OrderBy_Average(bool async) + { + await AssertQueryScalar( + async, + ss => from o in ss.Set() + group o by new { o.CustomerID } + into g + select g.OrderBy(e => e.OrderID).Select(e => (int?)e.OrderID).Average()); + + AssertSql( + """ +SELECT avg(o."OrderID"::double precision ORDER BY o."OrderID" NULLS FIRST) +FROM "Orders" AS o +GROUP BY o."CustomerID" +"""); + } + + #endregion GroupByAndDistinctWithCorrelatedCollection + + // See aggregate tests over TimeSpan in GearsOfWarQueryNpsgqlTest + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..a3e9ef7884 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryGaussDBTest.cs @@ -0,0 +1,18 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindIncludeNoTrackingQueryGaussDBTest : NorthwindIncludeNoTrackingQueryTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindIncludeNoTrackingQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public override async Task Include_collection_with_last_no_orderby(bool async) + => Assert.Equal( + RelationalStrings.LastUsedWithoutOrderBy(nameof(Enumerable.Last)), + (await Assert.ThrowsAsync( + () => base.Include_collection_with_last_no_orderby(async))).Message); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindIncludeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindIncludeQueryGaussDBTest.cs new file mode 100644 index 0000000000..f4641c854b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindIncludeQueryGaussDBTest.cs @@ -0,0 +1,12 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindIncludeQueryGaussDBTest : NorthwindIncludeQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindIncludeQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindJoinQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindJoinQueryGaussDBTest.cs new file mode 100644 index 0000000000..08b8e482e7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindJoinQueryGaussDBTest.cs @@ -0,0 +1,20 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindJoinQueryGaussDBTest : NorthwindJoinQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindJoinQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // #2759 + public override Task Join_local_collection_int_closure_is_cached_correctly(bool async) + => base.Join_local_collection_int_closure_is_cached_correctly(async); + // => Assert.ThrowsAsync(() => base.Join_local_collection_int_closure_is_cached_correctly(async)); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindKeylessEntitiesQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindKeylessEntitiesQueryGaussDBTest.cs new file mode 100644 index 0000000000..7051e65ada --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindKeylessEntitiesQueryGaussDBTest.cs @@ -0,0 +1,31 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindKeylessEntitiesQueryGaussDBTest : NorthwindKeylessEntitiesQueryRelationalTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindKeylessEntitiesQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task KeylessEntity_with_nav_defining_query(bool async) + { + // FromSql mapping. Issue #21627. + await Assert.ThrowsAsync(() => base.KeylessEntity_with_nav_defining_query(async)); + + AssertSql( + """ +SELECT c."CompanyName", c."OrderCount", c."SearchTerm" +FROM "CustomerQueryWithQueryFilter" AS c +WHERE c."OrderCount" > 0 +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindMiscellaneousQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindMiscellaneousQueryGaussDBTest.cs new file mode 100644 index 0000000000..a81a116e39 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindMiscellaneousQueryGaussDBTest.cs @@ -0,0 +1,498 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class NorthwindMiscellaneousQueryGaussDBTest : NorthwindMiscellaneousQueryRelationalTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindMiscellaneousQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // https://github.com/dotnet/efcore/issues/36311 + public override Task Entity_equality_contains_with_list_of_null(bool async) + => Assert.ThrowsAsync(() => base.Entity_equality_contains_with_list_of_null(async)); + + public override async Task Query_expression_with_to_string_and_contains(bool async) + { + await base.Query_expression_with_to_string_and_contains(async); + + AssertSql( + """ +SELECT o."CustomerID" +FROM "Orders" AS o +WHERE o."OrderDate" IS NOT NULL AND COALESCE(o."EmployeeID"::text, '') LIKE '%7%' +"""); + } + + public override async Task Select_expression_date_add_year(bool async) + { + await base.Select_expression_date_add_year(async); + + AssertSql( + """ +SELECT o."OrderDate" + INTERVAL '1 years' AS "OrderDate" +FROM "Orders" AS o +WHERE o."OrderDate" IS NOT NULL +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Select_expression_date_add_year_param(bool async) + { + var years = 2; + + await AssertQuery( + async, + ss => ss.Set().Where(o => o.OrderDate != null) + .Select( + o => new Order { OrderDate = o.OrderDate.Value.AddYears(years) }), + e => e.OrderDate); + + AssertSql( + """ +@years='2' + +SELECT o."OrderDate" + CAST(@years::text || ' years' AS interval) AS "OrderDate" +FROM "Orders" AS o +WHERE o."OrderDate" IS NOT NULL +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task DateTime_subtract_TimeSpan(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => o.OrderDate - TimeSpan.FromDays(1) == new DateTime(1997, 10, 8))); + + AssertSql( + """ +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Orders" AS o +WHERE o."OrderDate" - INTERVAL '1 00:00:00' = TIMESTAMP '1997-10-08T00:00:00' +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task DateTimeFunction_subtract_DateTime(bool async) + { + await AssertFirst( + async, + ss => ss.Set().Where(o => o.OrderDate != null) + .Select(o => new { Elapsed = (DateTime.Today - ((DateTime)o.OrderDate).Date).Days })); + + AssertSql( + """ +SELECT floor(date_part('day', date_trunc('day', now()::timestamp) - date_trunc('day', o."OrderDate")))::int AS "Elapsed" +FROM "Orders" AS o +WHERE o."OrderDate" IS NOT NULL +LIMIT 1 +"""); + } + + public override Task Add_minutes_on_constant_value(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.OrderID < 10500) + .OrderBy(o => o.OrderID) + .Select(o => new { Test = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(o.OrderID % 25) }), + assertOrder: true, + elementAsserter: (e, a) => AssertEqual(e.Test, a.Test)); + + public override async Task Client_code_using_instance_method_throws(bool async) + { + Assert.Equal( + CoreStrings.ClientProjectionCapturingConstantInMethodInstance( + "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryGaussDBTest", + "InstanceMethod"), + (await Assert.ThrowsAsync( + () => base.Client_code_using_instance_method_throws(async))).Message); + + AssertSql(); + } + + public override async Task Client_code_using_instance_in_static_method(bool async) + { + Assert.Equal( + CoreStrings.ClientProjectionCapturingConstantInMethodArgument( + "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryGaussDBTest", + "StaticMethod"), + (await Assert.ThrowsAsync( + () => base.Client_code_using_instance_in_static_method(async))).Message); + + AssertSql(); + } + + public override async Task Client_code_using_instance_in_anonymous_type(bool async) + { + Assert.Equal( + CoreStrings.ClientProjectionCapturingConstantInTree( + "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryGaussDBTest"), + (await Assert.ThrowsAsync( + () => base.Client_code_using_instance_in_anonymous_type(async))).Message); + + AssertSql(); + } + + public override async Task Client_code_unknown_method(bool async) + { + await AssertTranslationFailedWithDetails( + () => base.Client_code_unknown_method(async), + CoreStrings.QueryUnableToTranslateMethod( + "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryTestBase>", + nameof(UnknownMethod))); + + AssertSql(); + } + + public override async Task Max_on_empty_sequence_throws(bool async) + { + await Assert.ThrowsAsync(() => base.Max_on_empty_sequence_throws(async)); + + AssertSql( + """ +SELECT ( + SELECT max(o."OrderID") + FROM "Orders" AS o + WHERE c."CustomerID" = o."CustomerID") AS "Max" +FROM "Customers" AS c +"""); + } + + public override async Task Entity_equality_through_subquery_composite_key(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Entity_equality_through_subquery_composite_key(async))).Message; + + Assert.Equal( + CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported("==", nameof(OrderDetail)), + message); + + AssertSql(); + } + + public override async Task + Select_DTO_constructor_distinct_with_collection_projection_translated_to_server_with_binding_after_client_eval(bool async) + { + // Allow binding of expressions after projection has turned to client eval. Issue #24478. + await Assert.ThrowsAsync( + () => base + .Select_DTO_constructor_distinct_with_collection_projection_translated_to_server_with_binding_after_client_eval(async)); + + AssertSql( + """ +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 o0 +LEFT JOIN "Orders" AS o1 ON o0."CustomerID" = o1."CustomerID" +ORDER BY o0."CustomerID" NULLS FIRST +"""); + } + + // TODO: #3406 + public override Task Where_nanosecond_and_microsecond_component(bool async) + => AssertTranslationFailed(() => base.Where_nanosecond_and_microsecond_component(async)); + + // TODO: Array tests can probably move to the dedicated ArrayQueryTest suite + + #region Array contains + + // Note that this also takes care of array.Any(x => x == y) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_Contains_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(c => new[] { "ALFKI", "ANATR" }.Contains(c.CustomerID))); + + // Note: for constant lists there's no advantage in using the GaussDB-specific x = ANY (a b, c), unlike + // for parameterized lists. + + 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" IN ('ALFKI', 'ANATR') +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_Contains_parameter(bool async) + { + var regions = new[] { "UK", "SP" }; + + await AssertQuery( + async, + ss => ss.Set().Where(c => regions.Contains(c.Region))); + + // Instead of c."Region" IN ('UK', 'SP') we generate the GaussDB-specific x = ANY (a, b, c), which can + // be parameterized. + // Ideally parameter sniffing would allow us to produce SQL without the null check since the regions array doesn't contain one + // (see https://github.com/aspnet/EntityFrameworkCore/issues/17598). + AssertSql( + """ +@regions={ 'UK', 'SP' } (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."Region" = ANY (@regions) OR (c."Region" IS NULL AND array_position(@regions, NULL) IS NOT NULL) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_Contains_parameter_with_null(bool async) + { + var regions = new[] { "UK", "SP", null }; + + await AssertQuery( + async, + ss => ss.Set().Where(c => regions.Contains(c.Region))); + + // Instead of c."Region" IN ('UK', 'SP') we generate the GaussDB-specific x = ANY (a, b, c), which can + // be parameterized. + // Ideally parameter sniffing would allow us to produce SQL with an optimized null check (no need to check the array server-side) + // (see https://github.com/aspnet/EntityFrameworkCore/issues/17598). + AssertSql( + """ +@regions={ 'UK', 'SP', NULL } (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."Region" = ANY (@regions) OR (c."Region" IS NULL AND array_position(@regions, NULL) IS NOT NULL) +"""); + } + + #endregion Array contains + + #region Any/All Like + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_Any_Like(bool async) + { + await using var context = CreateContext(); + + var collection = new[] { "A%", "B%", "C%" }; + var query = context.Set().Where(c => collection.Any(y => EF.Functions.Like(c.Address, y))); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.Equal( + [ + "ANATR", + "BERGS", + "BOLID", + "CACTU", + "COMMI", + "CONSH", + "FISSA", + "FRANK", + "GODOS", + "GOURL", + "HILAA", + "HUNGC", + "LILAS", + "LINOD", + "PERIC", + "QUEEN", + "RANCH", + "RICAR", + "SUPRD", + "TORTU", + "TRADH", + "WANDK" + ], result.Select(e => e.CustomerID)); + + AssertSql( + """ +@collection={ '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) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_All_Like(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.Empty(result); + + AssertSql( + """ +@collection={ '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) +"""); + } + + [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={ '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 NOT (c."Address" LIKE ALL (@collection)) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_Any_ILike(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( + [ + "ANATR", + "BERGS", + "BOLID", + "CACTU", + "COMMI", + "CONSH", + "FISSA", + "FRANK", + "GODOS", + "GOURL", + "HILAA", + "HUNGC", + "LILAS", + "LINOD", + "PERIC", + "QUEEN", + "RANCH", + "RICAR", + "SUPRD", + "TORTU", + "TRADH", + "WANDK" + ], result.Select(e => e.CustomerID)); + + AssertSql( + """ +@collection={ '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) +"""); + } + + [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={ '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 NOT (c."Address" ILIKE ANY (@collection)) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Array_All_ILike(bool async) + { + await using var context = CreateContext(); + + var collection = new[] { "a%", "b%", "c%" }; + var query = context.Set().Where(c => collection.All(y => EF.Functions.ILike(c.Address, y))); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.Empty(result); + + AssertSql( + """ +@collection={ '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) +"""); + } + + #endregion Any/All Like + + [ConditionalFact] // #1560 + public async Task Lateral_join_with_table_is_rewritten_with_subquery() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers.Select(c1 => ctx.Customers.Select(c2 => c2.ContactName).ToList()).ToListAsync(); + + AssertSql( + """ +SELECT c."CustomerID", c0."ContactName", c0."CustomerID" +FROM "Customers" AS c +LEFT JOIN LATERAL (SELECT * FROM "Customers") AS c0 ON TRUE +ORDER BY c."CustomerID" NULLS FIRST +"""); + } + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindNavigationsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindNavigationsQueryGaussDBTest.cs new file mode 100644 index 0000000000..3e08fd511b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindNavigationsQueryGaussDBTest.cs @@ -0,0 +1,15 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindNavigationsQueryGaussDBTest : NorthwindNavigationsQueryRelationalTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindNavigationsQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryFiltersQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryFiltersQueryGaussDBTest.cs new file mode 100644 index 0000000000..f5452025c8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryFiltersQueryGaussDBTest.cs @@ -0,0 +1,15 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindQueryFiltersQueryGaussDBTest : NorthwindQueryFiltersQueryTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindQueryFiltersQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryGaussDBFixture.cs new file mode 100644 index 0000000000..772ffb916c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryGaussDBFixture.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindQueryGaussDBFixture : NorthwindQueryRelationalFixture + where TModelCustomizer : ITestModelCustomizer, new() +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBNorthwindTestStoreFactory.Instance; + + protected override Type ContextType + => typeof(NorthwindGaussDBContext); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new GaussDBDbContextOptionsBuilder(optionsBuilder).SetPostgresVersion(TestEnvironment.PostgresVersion); + return optionsBuilder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // Note that we map price properties to numeric(12,2) columns, not to money as in SqlServer, since in + // PG, money is discouraged/obsolete and various tests fail with it. + + modelBuilder.Entity( + b => + { + b.Property(o => o.EmployeeID).HasColumnType("int"); + b.Property(o => o.OrderDate).HasColumnType("timestamp without time zone"); + }); + + modelBuilder.Entity( + b => + { + b.Property(c => c.EmployeeID).HasColumnType("int"); + b.Property(c => c.ReportsTo).HasColumnType("int"); + }); + + modelBuilder.Entity() + .Property(o => o.EmployeeID) + .HasColumnType("int"); + + modelBuilder.Entity() + .Property(p => p.UnitsInStock) + .HasColumnType("smallint"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryTaggingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryTaggingQueryGaussDBTest.cs new file mode 100644 index 0000000000..1c7c2766fa --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindQueryTaggingQueryGaussDBTest.cs @@ -0,0 +1,14 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindQueryTaggingQueryGaussDBTest : NorthwindQueryTaggingQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindQueryTaggingQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSelectQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSelectQueryGaussDBTest.cs new file mode 100644 index 0000000000..45e3c60dff --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSelectQueryGaussDBTest.cs @@ -0,0 +1,56 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSelectQueryGaussDBTest : NorthwindSelectQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindSelectQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_datetime_DayOfWeek_component(bool async) + { + await base.Select_datetime_DayOfWeek_component(async); + + AssertSql( + """ +SELECT floor(date_part('dow', o."OrderDate"))::int +FROM "Orders" AS o +"""); + } + + public override async Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) + { + // Identifier set for Distinct. Issue #24440. + Assert.Equal( + RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(async))) + .Message); + + AssertSql(); + } + + public override async Task + SelectMany_with_collection_being_correlated_subquery_which_references_non_mapped_properties_from_inner_and_outer_entity( + bool async) + { + await AssertUnableToTranslateEFProperty( + () => base + .SelectMany_with_collection_being_correlated_subquery_which_references_non_mapped_properties_from_inner_and_outer_entity( + async)); + + AssertSql(); + } + + public override Task Member_binding_after_ctor_arguments_fails_with_client_eval(bool async) + => AssertTranslationFailed(() => base.Member_binding_after_ctor_arguments_fails_with_client_eval(async)); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSetOperationsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSetOperationsQueryGaussDBTest.cs new file mode 100644 index 0000000000..28717a1851 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSetOperationsQueryGaussDBTest.cs @@ -0,0 +1,195 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSetOperationsQueryGaussDBTest + : NorthwindSetOperationsQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindSetOperationsQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Leftmost_nulls_with_two_unions(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) + .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); + + AssertSql( + """ +SELECT NULL AS "OrderID", o."CustomerID" +FROM "Orders" AS o +UNION +SELECT o0."OrderID", o0."CustomerID" +FROM "Orders" AS o0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Leftmost_nulls_with_three_unions(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); + + AssertSql( + """ +SELECT NULL::int AS "OrderID", o."CustomerID" +FROM "Orders" AS o +UNION +SELECT NULL AS "OrderID", o0."CustomerID" +FROM "Orders" AS o0 +UNION +SELECT o1."OrderID", o1."CustomerID" +FROM "Orders" AS o1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Leftmost_nulls_with_four_unions(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); + + AssertSql( + """ +SELECT NULL::int AS "OrderID", o."CustomerID" +FROM "Orders" AS o +UNION +SELECT NULL AS "OrderID", o0."CustomerID" +FROM "Orders" AS o0 +UNION +SELECT NULL AS "OrderID", o1."CustomerID" +FROM "Orders" AS o1 +UNION +SELECT o2."OrderID", o2."CustomerID" +FROM "Orders" AS o2 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Leftmost_nulls_with_five_unions(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); + + AssertSql( + """ +SELECT NULL::int AS "OrderID", o."CustomerID" +FROM "Orders" AS o +UNION +SELECT NULL AS "OrderID", o0."CustomerID" +FROM "Orders" AS o0 +UNION +SELECT NULL AS "OrderID", o1."CustomerID" +FROM "Orders" AS o1 +UNION +SELECT NULL AS "OrderID", o2."CustomerID" +FROM "Orders" AS o2 +UNION +SELECT o3."OrderID", o3."CustomerID" +FROM "Orders" AS o3 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Leftmost_nulls_in_tables_and_predicate(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID })) + .Where( + o => o.CustomerID + == ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) + .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) + .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID })) + .OrderBy(o => o.CustomerID) + .First() + .CustomerID)); + + AssertSql( + """ +SELECT u0."OrderID", u0."CustomerID" +FROM ( + SELECT NULL::int AS "OrderID", o."CustomerID" + FROM "Orders" AS o + UNION + SELECT NULL AS "OrderID", o0."CustomerID" + FROM "Orders" AS o0 + UNION + SELECT o1."OrderID", o1."CustomerID" + FROM "Orders" AS o1 +) AS u0 +WHERE u0."CustomerID" = ( + SELECT u2."CustomerID" + FROM ( + SELECT NULL::int AS "OrderID", o2."CustomerID" + FROM "Orders" AS o2 + UNION + SELECT NULL AS "OrderID", o3."CustomerID" + FROM "Orders" AS o3 + UNION + SELECT o4."OrderID", o4."CustomerID" + FROM "Orders" AS o4 + ) 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 + UNION + SELECT NULL AS "OrderID", o3."CustomerID" + FROM "Orders" AS o3 + UNION + SELECT o4."OrderID", o4."CustomerID" + FROM "Orders" AS o4 + ) AS u2 + ORDER BY u2."CustomerID" NULLS FIRST + LIMIT 1) IS NULL) +"""); + } + + public override async Task Client_eval_Union_FirstOrDefault(bool async) + { + // Client evaluation in projection. Issue #16243. + Assert.Equal( + RelationalStrings.SetOperationsNotAllowedAfterClientEvaluation, + (await Assert.ThrowsAsync( + () => base.Client_eval_Union_FirstOrDefault(async))).Message); + + AssertSql(); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..ea54bba853 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQueryGaussDBTest.cs @@ -0,0 +1,14 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSplitIncludeNoTrackingQueryGaussDBTest : NorthwindSplitIncludeNoTrackingQueryTestBase< + NorthwindQueryGaussDBFixture> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindSplitIncludeNoTrackingQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + // TestSqlLoggerFactory.CaptureOutput(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSplitIncludeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSplitIncludeQueryGaussDBTest.cs new file mode 100644 index 0000000000..70a5d4ca08 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSplitIncludeQueryGaussDBTest.cs @@ -0,0 +1,13 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSplitIncludeQueryGaussDBTest : NorthwindSplitIncludeQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindSplitIncludeQueryGaussDBTest( + NorthwindQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + // TestSqlLoggerFactory.CaptureOutput(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSqlQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSqlQueryGaussDBTest.cs new file mode 100644 index 0000000000..f3f1254ebe --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindSqlQueryGaussDBTest.cs @@ -0,0 +1,75 @@ +using System.Data.Common; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSqlQueryGaussDBTest : NorthwindSqlQueryTestBase> +{ + public NorthwindSqlQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQueryRaw_over_int(bool async) + { + await base.SqlQueryRaw_over_int(async); + + AssertSql( + """ +SELECT "ProductID" FROM "Products" +"""); + } + + public override async Task SqlQuery_composed_Contains(bool async) + { + await base.SqlQuery_composed_Contains(async); + + AssertSql( + """ +SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" +FROM "Orders" AS o +WHERE o."OrderID" IN ( + SELECT s."Value" + FROM ( + SELECT "ProductID" AS "Value" FROM "Products" + ) AS s +) +"""); + } + + public override async Task SqlQuery_composed_Join(bool async) + { + await base.SqlQuery_composed_Join(async); + + AssertSql( + """ +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 s ON o."OrderID" = s."Value"::int +"""); + } + + public override async Task SqlQuery_over_int_with_parameter(bool async) + { + await base.SqlQuery_over_int_with_parameter(async); + + AssertSql( + """ +p0='10' + +SELECT "ProductID" FROM "Products" WHERE "ProductID" = @p0 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + protected override DbParameter CreateDbParameter(string name, object value) + => new GaussDBParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindStringIncludeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindStringIncludeQueryGaussDBTest.cs new file mode 100644 index 0000000000..c191715e24 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindStringIncludeQueryGaussDBTest.cs @@ -0,0 +1,18 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindStringIncludeQueryGaussDBTest + : NorthwindStringIncludeQueryTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindStringIncludeQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public override async Task Include_collection_with_last_no_orderby(bool async) + => Assert.Equal( + RelationalStrings.LastUsedWithoutOrderBy(nameof(Enumerable.Last)), + (await Assert.ThrowsAsync( + () => base.Include_collection_with_last_no_orderby(async))).Message); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindWhereQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindWhereQueryGaussDBTest.cs new file mode 100644 index 0000000000..7c1b849ed8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NorthwindWhereQueryGaussDBTest.cs @@ -0,0 +1,383 @@ +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindWhereQueryGaussDBTest : NorthwindWhereQueryRelationalTestBase> +{ + // ReSharper disable once UnusedParameter.Local + public NorthwindWhereQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // https://github.com/dotnet/efcore/issues/36311 + public override Task Where_navigation_contains(bool async) + => Assert.ThrowsAsync(() => base.Where_navigation_contains(async)); + + public override async Task Where_compare_constructed_equal(bool async) + { + // Anonymous type to constant comparison. Issue #14672. + await AssertTranslationFailed(() => base.Where_compare_constructed_equal(async)); + + AssertSql(); + } + + public override async Task Where_compare_constructed_multi_value_equal(bool async) + { + // Anonymous type to constant comparison. Issue #14672. + await AssertTranslationFailed(() => base.Where_compare_constructed_multi_value_equal(async)); + + AssertSql(); + } + + public override async Task Where_compare_constructed_multi_value_not_equal(bool async) + { + // Anonymous type to constant comparison. Issue #14672. + await AssertTranslationFailed(() => base.Where_compare_constructed_multi_value_not_equal(async)); + + AssertSql(); + } + + public override async Task Where_compare_tuple_constructed_equal(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(c => new Tuple(c.City).Equals(new Tuple("London")))); + + 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."City") = ('London') +"""); + } + + public override async Task Where_compare_tuple_constructed_multi_value_equal(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + c => new Tuple(c.City, c.Country).Equals(new Tuple("Sao Paulo", "Brazil")))); + + 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."City", c."Country") = ('Sao Paulo', 'Brazil') +"""); + } + + public override async Task Where_compare_tuple_constructed_multi_value_not_equal(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + c => !new Tuple(c.City, c.Country).Equals(new Tuple("Sao Paulo", "Brazil")))); + + 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."City" <> 'Sao Paulo' OR c."City" IS NULL OR c."Country" <> 'Brazil' OR c."Country" IS NULL +"""); + } + + public override async Task Where_compare_tuple_create_constructed_equal(bool async) + { + // Anonymous type to constant comparison. Issue #14672. + await AssertTranslationFailed(() => base.Where_compare_tuple_create_constructed_equal(async)); + + AssertSql(); + } + + public override async Task Where_compare_tuple_create_constructed_multi_value_equal(bool async) + { + // Anonymous type to constant comparison. Issue #14672. + await AssertTranslationFailed(() => base.Where_compare_tuple_create_constructed_multi_value_equal(async)); + + AssertSql(); + } + + public override async Task Where_compare_tuple_create_constructed_multi_value_not_equal(bool async) + { + // Anonymous type to constant comparison. Issue #14672. + await AssertTranslationFailed(() => base.Where_compare_tuple_create_constructed_multi_value_not_equal(async)); + + AssertSql(); + } + + #region Row values + + [ConditionalFact] + public async Task Row_value_GreaterThan() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where( + c => EF.Functions.GreaterThan( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") > ('Buenos Aires', 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_GreaterThan_with_differing_types() + { + await using var ctx = CreateContext(); + + _ = await ctx.Orders + .Where( + o => EF.Functions.GreaterThan( + ValueTuple.Create(o.CustomerID, o.OrderID), + ValueTuple.Create("ALFKI", 10702))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Orders" AS o +WHERE (o."CustomerID", o."OrderID") > ('ALFKI', 10702) +"""); + } + + [ConditionalFact] + public async Task Row_value_GreaterThan_with_parameter() + { + await using var ctx = CreateContext(); + + var city1 = "Buenos Aires"; + + _ = await ctx.Customers + .Where( + c => EF.Functions.GreaterThan( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create(city1, "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +@city1='Buenos Aires' + +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") > (@city1, 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_GreaterThan_with_parameter_with_ValueTuple_constructor() + { + await using var ctx = CreateContext(); + + var city1 = "Buenos Aires"; + + _ = await ctx.Customers + .Where( + c => EF.Functions.GreaterThan( + new ValueTuple(c.City, c.CustomerID), + new ValueTuple(city1, "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +@city1='Buenos Aires' + +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") > (@city1, 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_LessThan() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where( + c => EF.Functions.LessThan( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") < ('Buenos Aires', 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_GreaterThanOrEqual() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where( + c => EF.Functions.GreaterThanOrEqual( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") >= ('Buenos Aires', 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_LessThanOrEqual() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where( + c => EF.Functions.LessThanOrEqual( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") <= ('Buenos Aires', 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_with_ValueTuple_constructor() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where( + c => EF.Functions.GreaterThan( + new ValueTuple(c.City, c.CustomerID), + new ValueTuple("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") > ('Buenos Aires', 'OCEAN') +"""); + } + + [ConditionalFact] + public async Task Row_value_parameter_count_mismatch() + { + await using var ctx = CreateContext(); + + var exception = await Assert.ThrowsAsync( + () => ctx.Customers + .Where( + c => EF.Functions.LessThanOrEqual( + ValueTuple.Create(c.City, c.CustomerID), + ValueTuple.Create("Buenos Aires", "OCEAN", "foo"))) + .CountAsync()); + + Assert.Equal(GaussDBStrings.RowValueComparisonRequiresTuplesOfSameLength, exception.Message); + } + + [ConditionalFact] + public async Task Row_value_equals() + { + await using var ctx = CreateContext(); + + _ = await ctx.Customers + .Where( + c => + ValueTuple.Create(c.City, c.CustomerID).Equals( + ValueTuple.Create("Buenos Aires", "OCEAN"))) + .CountAsync(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE (c."City", c."CustomerID") = ('Buenos Aires', 'OCEAN') +"""); + } + + [ConditionalFact] + 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 +FROM "Customers" AS c +WHERE c."CustomerID" <> 'OCEAN' OR c."City" <> 'Buenos Aires' OR c."City" IS NULL +"""); + } + + [ConditionalFact] + public async Task Row_value_project() + { + await using var ctx = CreateContext(); + + var (customerId, orderDate) = await ctx.Orders + .Where(o => o.OrderID == 10248) + .Select(o => ValueTuple.Create(o.CustomerID, o.OrderDate)) + .SingleAsync(); + + Assert.Equal("VINET", customerId); + Assert.Equal(new DateTime(1996, 7, 4), orderDate); + + AssertSql( + """ +SELECT (o."CustomerID", o."OrderDate") +FROM "Orders" AS o +WHERE o."OrderID" = 10248 +LIMIT 2 +"""); + } + + #endregion Row values + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NullKeysGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NullKeysGaussDBTest.cs new file mode 100644 index 0000000000..0bd9a9ed06 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NullKeysGaussDBTest.cs @@ -0,0 +1,13 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class NullKeysGaussDBTest(NullKeysGaussDBTest.NullKeysGaussDBFixture fixture) + : NullKeysTestBase(fixture) +{ + public class NullKeysGaussDBFixture : NullKeysFixtureBase + { + protected override string StoreName { get; } = "StringsContext"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/NullSemanticsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/NullSemanticsQueryGaussDBTest.cs new file mode 100644 index 0000000000..e3f03417e7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/NullSemanticsQueryGaussDBTest.cs @@ -0,0 +1,206 @@ +using Microsoft.EntityFrameworkCore.TestModels.NullSemanticsModel; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace Microsoft.EntityFrameworkCore.Query; + +// ReSharper disable once UnusedMember.Global +public class NullSemanticsQueryGaussDBTest : NullSemanticsQueryTestBase +{ + public NullSemanticsQueryGaussDBTest(NullSemanticsQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Compare_row_values_equal_without_expansion(bool async) + { + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.NullableStringB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => ValueTuple.Create(e.IntA, e.NullableStringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) + .Select(e => e.Id)); + + AssertSql( + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."StringA") = (e."IntB", e."StringB") +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."StringA") = (e."IntB", e."NullableStringB") +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."NullableStringA") = (e."IntB", e."StringB") +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Compare_row_values_equal_with_expansion(bool async) + { + await AssertQueryScalar( + async, ss => ss.Set() + .Where( + e => ValueTuple.Create(e.NullableStringA, e.IntA, e.BoolA) + .Equals(ValueTuple.Create(e.NullableStringB, e.IntB, e.BoolB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where( + e => ValueTuple.Create(e.IntA, e.NullableStringA, e.BoolA) + .Equals(ValueTuple.Create(e.IntB, e.NullableStringB, e.BoolB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where( + e => ValueTuple.Create(e.IntA, e.StringA, e.NullableBoolA) + .Equals(ValueTuple.Create(e.IntB, e.StringB, e.NullableBoolB))) + .Select(e => e.Id)); + + AssertSql( + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."BoolA") = (e."IntB", e."BoolB") AND (e."NullableStringA" = e."NullableStringB" OR (e."NullableStringA" IS NULL AND e."NullableStringB" IS NULL)) +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."BoolA") = (e."IntB", e."BoolB") AND (e."NullableStringA" = e."NullableStringB" OR (e."NullableStringA" IS NULL AND e."NullableStringB" IS NULL)) +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."StringA") = (e."IntB", e."StringB") AND (e."NullableBoolA" = e."NullableBoolB" OR (e."NullableBoolA" IS NULL AND e."NullableBoolB" IS NULL)) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Compare_row_values_not_equal(bool async) + { + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => !ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => !ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.NullableStringB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => !ValueTuple.Create(e.IntA, e.NullableStringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where(e => !ValueTuple.Create(e.IntA, e.NullableStringA).Equals(ValueTuple.Create(e.IntB, e.NullableStringB))) + .Select(e => e.Id)); + + await AssertQueryScalar( + async, ss => ss.Set() + .Where( + e => !ValueTuple.Create(e.IntA, e.StringA, e.NullableBoolA) + .Equals(ValueTuple.Create(e.IntB, e.StringB, e.NullableBoolB))) + .Select(e => e.Id)); + + AssertSql( + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."StringA") <> (e."IntB", e."StringB") +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE e."IntA" <> e."IntB" OR e."StringA" <> e."NullableStringB" OR e."NullableStringB" IS NULL +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE e."IntA" <> e."IntB" OR e."NullableStringA" <> e."StringB" OR e."NullableStringA" IS NULL +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE e."IntA" <> e."IntB" OR ((e."NullableStringA" <> e."NullableStringB" OR e."NullableStringA" IS NULL OR e."NullableStringB" IS NULL) AND (e."NullableStringA" IS NOT NULL OR e."NullableStringB" IS NOT NULL)) +""", + // + """ +SELECT e."Id" +FROM "Entities1" AS e +WHERE (e."IntA", e."StringA") <> (e."IntB", e."StringB") OR ((e."NullableBoolA" <> e."NullableBoolB" OR e."NullableBoolA" IS NULL OR e."NullableBoolB" IS NULL) AND (e."NullableBoolA" IS NOT NULL OR e."NullableBoolB" IS NOT NULL)) +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + protected override NullSemanticsContext CreateContext(bool useRelationalNulls = false) + { + var options = new DbContextOptionsBuilder(Fixture.CreateOptions()); + if (useRelationalNulls) + { + new GaussDBDbContextOptionsBuilder(options).UseRelationalNulls(); + } + + var context = new NullSemanticsContext(options.Options); + + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + return context; + } + + public class NullSemanticsQueryGaussDBFixture : NullSemanticsQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.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 GaussDBBoolTypeMapping instead. + modelBuilder.HasDbFunction( + typeof(NullSemanticsQueryFixtureBase).GetMethod(nameof(BoolSwitch))!, + b => b.HasTranslation(args => new CaseExpression( + operand: args[0], + [ + new CaseWhenClause(new SqlConstantExpression(true, typeMapping: GaussDBBoolTypeMapping.Default), args[1]), + new CaseWhenClause(new SqlConstantExpression(false, typeMapping: GaussDBBoolTypeMapping.Default), args[2]) + ]))); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/OperatorsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/OperatorsQueryGaussDBTest.cs new file mode 100644 index 0000000000..9277a7fd30 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/OperatorsQueryGaussDBTest.cs @@ -0,0 +1,199 @@ +using Microsoft.EntityFrameworkCore.TestModels.Operators; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class OperatorsQueryGaussDBTest(NonSharedFixture fixture) : OperatorsQueryTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + public override async Task Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false() + { + await base.Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false(); + + 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( + """ +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( + """ +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() + { + await base.Or_on_two_nested_binaries_and_another_simple_comparison(); + + AssertSql( + """ +SELECT o."Id" AS "Id1", o0."Id" AS "Id2", o1."Id" AS "Id3", o2."Id" AS "Id4", o3."Id" AS "Id5" +FROM "OperatorEntityString" AS o +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 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 +"""); + } + + public override async Task Projection_with_not_and_negation_on_integer() + { + await base.Projection_with_not_and_negation_on_integer(); + + AssertSql( + """ +SELECT (~(-(-(o1."Value" + o."Value" + 2)))) % (-(o0."Value" + o0."Value") - o."Value") +FROM "OperatorEntityLong" AS o +CROSS JOIN "OperatorEntityLong" AS o0 +CROSS JOIN "OperatorEntityLong" AS o1 +ORDER BY o."Id" NULLS FIRST, o0."Id" NULLS FIRST, o1."Id" NULLS FIRST +"""); + } + + public override async Task Negate_on_column(bool async) + { + await base.Negate_on_column(async); + + AssertSql( + """ +SELECT o."Id" +FROM "OperatorEntityInt" AS o +WHERE o."Id" = -o."Value" +"""); + } + + public override async Task Double_negate_on_column() + { + await base.Double_negate_on_column(); + + AssertSql( + """ +SELECT o."Id" +FROM "OperatorEntityInt" AS o +WHERE -(-o."Value") = o."Value" +"""); + } + + public override async Task Negate_on_binary_expression(bool async) + { + await base.Negate_on_binary_expression(async); + + AssertSql( + """ +SELECT o."Id" AS "Id1", o0."Id" AS "Id2" +FROM "OperatorEntityInt" AS o +CROSS JOIN "OperatorEntityInt" AS o0 +WHERE -o."Value" = -(o."Id" + o0."Value") +"""); + } + + public override async Task Negate_on_like_expression(bool async) + { + await base.Negate_on_like_expression(async); + + AssertSql( + """ +SELECT o."Id" +FROM "OperatorEntityString" AS o +WHERE o."Value" NOT LIKE 'A%' OR o."Value" IS NULL +"""); + } + + public override async Task Concat_and_json_scalar(bool async) + { + await base.Concat_and_json_scalar(async); + + AssertSql( + """ +SELECT o."Id", o."Owned" +FROM "Owner" AS o +WHERE 'Foo' || (o."Owned" ->> 'SomeProperty') = 'FooBar' +LIMIT 2 +"""); + } + + [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); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableInt); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesLong); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesBool); + ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableBool); + // ctx.Set().AddRange(ExpectedData.OperatorEntitiesDateTimeOffset); + + await ctx.SaveChangesAsync(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/OptionalDependentQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/OptionalDependentQueryGaussDBTest.cs new file mode 100644 index 0000000000..0ffa0772dc --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/OptionalDependentQueryGaussDBTest.cs @@ -0,0 +1,159 @@ +using Microsoft.EntityFrameworkCore.TestModels.OptionalDependent; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class OptionalDependentQueryGaussDBTest : OptionalDependentQueryTestBase< + OptionalDependentQueryGaussDBTest.OptionalDependentQueryGaussDBFixture> +{ + public OptionalDependentQueryGaussDBTest(OptionalDependentQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Basic_projection_entity_with_all_optional(bool async) + { + await base.Basic_projection_entity_with_all_optional(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesAllOptional" AS e +"""); + } + + public override async Task Basic_projection_entity_with_some_required(bool async) + { + await base.Basic_projection_entity_with_some_required(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesSomeRequired" AS e +"""); + } + + public override async Task Filter_optional_dependent_with_all_optional_compared_to_null(bool async) + { + await base.Filter_optional_dependent_with_all_optional_compared_to_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesAllOptional" AS e +WHERE (e."Json") IS NULL +"""); + } + + public override async Task Filter_optional_dependent_with_all_optional_compared_to_not_null(bool async) + { + await base.Filter_optional_dependent_with_all_optional_compared_to_not_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesAllOptional" AS e +WHERE (e."Json") IS NOT NULL +"""); + } + + public override async Task Filter_optional_dependent_with_some_required_compared_to_null(bool async) + { + await base.Filter_optional_dependent_with_some_required_compared_to_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesSomeRequired" AS e +WHERE (e."Json") IS NULL +"""); + } + + public override async Task Filter_optional_dependent_with_some_required_compared_to_not_null(bool async) + { + await base.Filter_optional_dependent_with_some_required_compared_to_not_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesSomeRequired" AS e +WHERE (e."Json") IS NOT NULL +"""); + } + + public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_null(bool async) + { + await base.Filter_nested_optional_dependent_with_all_optional_compared_to_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesAllOptional" AS e +WHERE (e."Json" ->> 'OpNav1') IS NULL +"""); + } + + public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(bool async) + { + await base.Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesAllOptional" AS e +WHERE (e."Json" ->> 'OpNav2') IS NOT NULL +"""); + } + + public override async Task Filter_nested_optional_dependent_with_some_required_compared_to_null(bool async) + { + await base.Filter_nested_optional_dependent_with_some_required_compared_to_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesSomeRequired" AS e +WHERE (e."Json" ->> 'ReqNav1') IS NULL +"""); + } + + public override async Task Filter_nested_optional_dependent_with_some_required_compared_to_not_null(bool async) + { + await base.Filter_nested_optional_dependent_with_some_required_compared_to_not_null(async); + + AssertSql( + """ +SELECT e."Id", e."Name", e."Json" +FROM "EntitiesSomeRequired" AS e +WHERE (e."Json" ->> 'ReqNav2') IS NOT NULL +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class OptionalDependentQueryGaussDBFixture : OptionalDependentQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // The EF seed data has Unspecified DateTimes, but we map DateTime to timestamptz by default, which requires UTC DateTimes. + // Configure the properties to have timestamp, which allows Unspecified DateTimes. + modelBuilder.Entity().OwnsOne( + x => x.Json, + b => b.OwnsOne(x => x.OpNav2, b2 => b2.Property(op => op.ReqNested2).HasColumnType("timestamp without time zone"))); + + modelBuilder.Entity().OwnsOne( + x => x.Json, b => + { + b.OwnsOne(x => x.OpNav2, b2 => b2.Property(op => op.ReqNested2).HasColumnType("timestamp without time zone")); + b.OwnsOne(x => x.ReqNav2, b2 => b2.Property(op => op.ReqNested2).HasColumnType("timestamp without time zone")); + }); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/OwnedEntityQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/OwnedEntityQueryGaussDBTest.cs new file mode 100644 index 0000000000..ee4b8245d1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/OwnedEntityQueryGaussDBTest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class OwnedEntityQueryGaussDBTest(NonSharedFixture fixture) : OwnedEntityQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs new file mode 100644 index 0000000000..97a487c249 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs @@ -0,0 +1,22 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class OwnedQueryGaussDBTest(OwnedQueryGaussDBTest.OwnedQueryGaussDBFixture fixture) + : OwnedQueryRelationalTestBase(fixture) +{ + public class OwnedQueryGaussDBFixture : RelationalOwnedQueryFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity( + eb => eb.OwnsMany( + p => p.Orders, ob => ob.IndexerProperty("OrderDate").HasColumnType("timestamp without time zone"))); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/PrecompiledQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/PrecompiledQueryGaussDBTest.cs new file mode 100644 index 0000000000..cddda02144 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/PrecompiledQueryGaussDBTest.cs @@ -0,0 +1,2108 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class PrecompiledQueryGaussDBTest( + PrecompiledQueryGaussDBTest.PrecompiledQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : PrecompiledQueryRelationalTestBase(fixture, testOutputHelper), + IClassFixture +{ + protected override bool AlwaysPrintGeneratedSources + => false; + + #region Expression types + + public override async Task BinaryExpression() + { + await base.BinaryExpression(); + + AssertSql( + """ +@id='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" > @id +"""); + } + + 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='yes' + +SELECT CASE + WHEN b."Id" = 2 THEN @yes + 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_startswith='foo%' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @pattern_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='8' + +SELECT @id 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='8' + +SELECT ARRAY[b."Id",b."Id" + @i]::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 Regular operators + + 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_with_constant() + { + await base.Skip_with_constant(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +OFFSET @p +"""); + } + + public override async Task Skip_with_parameter() + { + await base.Skip_with_parameter(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +OFFSET @p +"""); + } + + public override async Task Take_with_constant() + { + await base.Take_with_constant(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +LIMIT @p +"""); + } + + public override async Task Take_with_parameter() + { + await base.Take_with_parameter(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Name" NULLS FIRST +LIMIT @p +"""); + } + + public override async Task Select_changes_type() + { + await base.Select_changes_type(); + + AssertSql( + """ +SELECT b."Name" +FROM "Blogs" AS b +"""); + } + + public override async Task Select_anonymous_object() + { + await base.Select_anonymous_object(); + + AssertSql( + """ +SELECT COALESCE(b."Name", '') || 'Foo' AS "Foo" +FROM "Blogs" AS b +"""); + } + + 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 +"""); + } + + #endregion Regular operators + + #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='8' + +SELECT @p IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +""", + // + """ +@p='7' + +SELECT @p IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +"""); + } + + public override async Task Terminating_ContainsAsync() + { + await base.Terminating_ContainsAsync(); + + AssertSql( + """ +@p='8' + +SELECT @p IN ( + SELECT b."Id" + FROM "Blogs" AS b +) +""", + // + """ +@p='7' + +SELECT @p 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='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +""", + // + """ +@p='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +"""); + } + + public override async Task Terminating_ElementAtAsync() + { + await base.Terminating_ElementAtAsync(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +""", + // + """ +@p='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +"""); + } + + public override async Task Terminating_ElementAtOrDefault() + { + await base.Terminating_ElementAtOrDefault(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +""", + // + """ +@p='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +"""); + } + + public override async Task Terminating_ElementAtOrDefaultAsync() + { + await base.Terminating_ElementAtOrDefaultAsync(); + + AssertSql( + """ +@p='1' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +""", + // + """ +@p='3' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +ORDER BY b."Id" NULLS FIRST +LIMIT 1 OFFSET @p +"""); + } + + 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_with_lambda() + { + await base.Terminating_ExecuteUpdate_with_lambda(); + + AssertSql( + """ +@suffix='Suffix' + +UPDATE "Blogs" AS b +SET "Name" = COALESCE(b."Name", '') || @suffix +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'Blog2Suffix' +"""); + } + + public override async Task Terminating_ExecuteUpdate_without_lambda() + { + await base.Terminating_ExecuteUpdate_without_lambda(); + + AssertSql( + """ +@newValue='NewValue' + +UPDATE "Blogs" AS b +SET "Name" = @newValue +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'NewValue' +"""); + } + + public override async Task Terminating_ExecuteUpdateAsync_with_lambda() + { + await base.Terminating_ExecuteUpdateAsync_with_lambda(); + + AssertSql( + """ +@suffix='Suffix' + +UPDATE "Blogs" AS b +SET "Name" = COALESCE(b."Name", '') || @suffix +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_without_lambda() + { + await base.Terminating_ExecuteUpdateAsync_without_lambda(); + + AssertSql( + """ +@newValue='NewValue' + +UPDATE "Blogs" AS b +SET "Name" = @newValue +WHERE b."Id" > 8 +""", + // + """ +SELECT count(*)::int +FROM "Blogs" AS b +WHERE b."Id" = 9 AND b."Name" = 'NewValue' +"""); + } + + public override async Task Terminating_with_cancellation_token() + { + await base.Terminating_with_cancellation_token(); + + 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 +"""); + } + + #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={ '1', '2', '3' } (DbType = Object) + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = ANY (@ids) +"""); + } + + 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 Captured variable handling + + public override async Task Two_captured_variables_in_same_lambda() + { + await base.Two_captured_variables_in_same_lambda(); + + AssertSql( + """ +@yes='yes' +@no='no' + +SELECT CASE + WHEN b."Id" = 3 THEN @yes + ELSE @no +END +FROM "Blogs" AS b +"""); + } + + public override async Task Two_captured_variables_in_different_lambdas() + { + await base.Two_captured_variables_in_different_lambdas(); + + AssertSql( + """ +@starts_startswith='Blog%' +@ends_endswith='%2' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @starts_startswith AND b."Name" LIKE @ends_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_startswith='X%' +@foo_endswith='%X' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @foo_startswith AND b."Name" LIKE @foo_endswith +"""); + } + + public override async Task Same_captured_variable_twice_in_different_lambdas() + { + await base.Same_captured_variable_twice_in_different_lambdas(); + + AssertSql( + """ +@foo_startswith='X%' +@foo_endswith='%X' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Name" LIKE @foo_startswith AND b."Name" LIKE @foo_endswith +"""); + } + + public override async Task Multiple_queries_with_captured_variables() + { + await base.Multiple_queries_with_captured_variables(); + + AssertSql( + """ +@id1='8' +@id2='9' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = @id1 OR b."Id" = @id2 +""", + // + """ +@id1='8' + +SELECT b."Id", b."Name", b."Json" +FROM "Blogs" AS b +WHERE b."Id" = @id1 +LIMIT 2 +"""); + } + + #endregion Captured variable handling + + #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 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 PrecompiledQueryGaussDBFixture : PrecompiledQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.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 GaussDBDbContextOptionsBuilder(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 => GaussDBPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/PrecompiledSqlPregenerationQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/PrecompiledSqlPregenerationQueryGaussDBTest.cs new file mode 100644 index 0000000000..cc61138f27 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/PrecompiledSqlPregenerationQueryGaussDBTest.cs @@ -0,0 +1,244 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +// ReSharper disable InconsistentNaming + +public class PrecompiledSqlPregenerationQueryGaussDBTest( + PrecompiledSqlPregenerationQueryGaussDBTest.PrecompiledSqlPregenerationQueryGaussDBFixture 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='8' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @id +"""); + } + + public override async Task Nullable_value_type() + { + await base.Nullable_value_type(); + + AssertSql( + """ +@id='8' (Nullable = true) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @id +"""); + } + + public override async Task Nullable_reference_type() + { + await base.Nullable_reference_type(); + + AssertSql( + """ +@name='bar' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name +"""); + } + + public override async Task Non_nullable_reference_type() + { + await base.Non_nullable_reference_type(); + + AssertSql( + """ +@name='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name +"""); + } + + public override async Task Nullable_and_non_nullable_value_types() + { + await base.Nullable_and_non_nullable_value_types(); + + AssertSql( + """ +@id1='8' (Nullable = true) +@id2='9' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Id" = @id1 OR b."Id" = @id2 +"""); + } + + public override async Task Two_nullable_reference_types() + { + await base.Two_nullable_reference_types(); + + AssertSql( + """ +@name1='foo' +@name2='bar' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name1 OR b."Name" = @name2 +"""); + } + + public override async Task Two_non_nullable_reference_types() + { + await base.Two_non_nullable_reference_types(); + + AssertSql( + """ +@name1='foo' (Nullable = false) +@name2='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name1 OR b."Name" = @name2 +"""); + } + + public override async Task Nullable_and_non_nullable_reference_types() + { + await base.Nullable_and_non_nullable_reference_types(); + + AssertSql( + """ +@name1='foo' +@name2='bar' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name1 OR b."Name" = @name2 +"""); + } + + public override async Task Too_many_nullable_parameters_prevent_pregeneration() + { + await base.Too_many_nullable_parameters_prevent_pregeneration(); + + AssertSql( + """ +@name1='foo' +@name2='bar' +@name3='baz' +@name4='baq' + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name1 OR b."Name" = @name2 OR b."Name" = @name3 OR b."Name" = @name4 +"""); + } + + public override async Task Many_non_nullable_parameters_do_not_prevent_pregeneration() + { + await base.Many_non_nullable_parameters_do_not_prevent_pregeneration(); + + AssertSql( + """ +@name1='foo' (Nullable = false) +@name2='bar' (Nullable = false) +@name3='baz' (Nullable = false) +@name4='baq' (Nullable = false) + +SELECT b."Id", b."Name" +FROM "Blogs" AS b +WHERE b."Name" = @name1 OR b."Name" = @name2 OR b."Name" = @name3 OR b."Name" = @name4 +"""); + } + + #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 PrecompiledSqlPregenerationQueryGaussDBFixture : PrecompiledSqlPregenerationQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.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 GaussDBDbContextOptionsBuilder(builder); + npgsqlOptionsBuilder + .ExecutionStrategy(d => new NonRetryingExecutionStrategy(d)); + return builder; + } + + public override PrecompiledQueryTestHelpers PrecompiledQueryTestHelpers => GaussDBPrecompiledQueryTestHelpers.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/PrimitiveCollectionsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/PrimitiveCollectionsQueryGaussDBTest.cs new file mode 100644 index 0000000000..ffc8f387f5 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/PrimitiveCollectionsQueryGaussDBTest.cs @@ -0,0 +1,2216 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class PrimitiveCollectionsQueryGaussDBTest : PrimitiveCollectionsQueryRelationalTestBase< + PrimitiveCollectionsQueryGaussDBTest.PrimitiveCollectionsQueryGaussDBFixture> +{ + public PrimitiveCollectionsQueryGaussDBTest(PrimitiveCollectionsQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Inline_collection_of_ints_Contains(bool async) + { + await base.Inline_collection_of_ints_Contains(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" IN (10, 999) +"""); + } + + public override async Task Inline_collection_of_nullable_ints_Contains(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInt" IN (10, 999) +"""); + } + + public override async Task Inline_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Inline_collection_of_nullable_ints_Contains_null(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInt" IS NULL OR p."NullableInt" = 999 +"""); + } + + 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) + { + await base.Inline_collection_Count_with_one_value(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (2::int)) AS v("Value") + WHERE v."Value" > p."Id") = 1 +"""); + } + + public override async Task Inline_collection_Count_with_two_values(bool async) + { + await base.Inline_collection_Count_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (2::int), (999)) AS v("Value") + WHERE v."Value" > p."Id") = 1 +"""); + } + + public override async Task Inline_collection_Count_with_three_values(bool async) + { + await base.Inline_collection_Count_with_three_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (2::int), (999), (1000)) AS v("Value") + WHERE v."Value" > p."Id") = 2 +"""); + } + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE FALSE +"""); + } + + public override async Task Inline_collection_Contains_with_one_value(bool async) + { + await base.Inline_collection_Contains_with_one_value(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" = 2 +"""); + } + + public override async Task Inline_collection_Contains_with_two_values(bool async) + { + await base.Inline_collection_Contains_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" IN (2, 999) +"""); + } + + public override async Task Inline_collection_Contains_with_three_values(bool async) + { + await base.Inline_collection_Contains_with_three_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" IN (2, 999, 1000) +"""); + } + + public override async Task Inline_collection_Contains_with_all_parameters(bool async) + { + await base.Inline_collection_Contains_with_all_parameters(async); + + AssertSql( + """ +@i='2' +@j='999' + +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" IN (@i, @j) +"""); + } + + public override async Task Inline_collection_Contains_with_constant_and_parameter(bool async) + { + await base.Inline_collection_Contains_with_constant_and_parameter(async); + + AssertSql( + """ +@j='999' + +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" IN (2, @j) +"""); + } + + public override async Task Inline_collection_Contains_with_mixed_value_types(bool async) + { + await base.Inline_collection_Contains_with_mixed_value_types(async); + + AssertSql( + """ +@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" IN (999, @i, 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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" IN (999, @i, p."Id", p."Id" + p."Int") +"""); + } + + public override async Task Inline_collection_Contains_as_Any_with_predicate(bool async) + { + await base.Inline_collection_Contains_as_Any_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" IN (2, 999) +"""); + } + + public override async Task Inline_collection_negated_Contains_as_All(bool async) + { + await base.Inline_collection_negated_Contains_as_All(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" NOT IN (2, 999) +"""); + } + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int", @i) = 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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int", @i) = 25 +"""); + } + + public override async Task Inline_collection_Max_with_three_values(bool async) + { + await base.Inline_collection_Max_with_three_values(async); + + AssertSql( + """ +@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int", @i) = 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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int", @i) = 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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE LEAST(30, p."Int", @i) = 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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE GREATEST(30, p."Int", @i) = 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" = @i +"""); + } + + 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='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (@i::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={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" = ANY (@p) +"""); + } + + 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={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(@p) AS p0(value) + WHERE p0.value > p."Id") = 2 +"""); + } + + public override async Task Parameter_collection_Count(bool async) + { + await base.Parameter_collection_Count(async); + + AssertSql( + """ +@ids={ '2', '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(@ids) AS i(value) + WHERE i.value > p."Id") = 1 +"""); + } + + public override async Task Parameter_collection_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_of_ints_Contains_int(async); + + AssertSql( + """ +@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@ints) +""", + // + """ +@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@ints) AND p."Int" = ANY (@ints) 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={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@ints) +""", + // + """ +@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@ints) AND p."Int" = ANY (@ints) 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', '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Enum" = ANY (@enums) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + 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={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@ints) +""", + // + """ +@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@ints) AND p."Int" = ANY (@ints) 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={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInt" = ANY (@ints) OR (p."NullableInt" IS NULL AND array_position(@ints, NULL) IS NOT NULL) +""", + // + """ +@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableInt" = ANY (@ints) AND p."NullableInt" = ANY (@ints) IS NOT NULL) AND (p."NullableInt" IS NOT NULL OR array_position(@ints, NULL) IS NULL) +"""); + } + + public override async Task Parameter_collection_of_nullable_ints_Contains_int(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_int(async); + + AssertSql( + """ +@nullableInts={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (@nullableInts) +""", + // + """ +@nullableInts={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."Int" = ANY (@nullableInts) AND p."Int" = ANY (@nullableInts) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int(bool async) + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int(async); + + AssertSql( + """ +@nullableInts={ 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInt" = ANY (@nullableInts) OR (p."NullableInt" IS NULL AND array_position(@nullableInts, NULL) IS NOT NULL) +""", + // + """ +@nullableInts={ 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableInt" = ANY (@nullableInts) AND p."NullableInt" = ANY (@nullableInts) IS NOT NULL) AND (p."NullableInt" IS NOT NULL OR array_position(@nullableInts, NULL) IS NULL) +"""); + } + + public override async Task Parameter_collection_of_structs_Contains_struct(bool async) + { + await base.Parameter_collection_of_structs_Contains_struct(async); + + AssertSql( + """ +@values={ '22', '33' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."WrappedId" = ANY (@values) +""", + // + """ +@values={ '11', '44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."WrappedId" = ANY (@values) AND p."WrappedId" = ANY (@values) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_of_strings_Contains_string(bool async) + { + await base.Parameter_collection_of_strings_Contains_string(async); + + AssertSql( + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."String" = ANY (@strings) +""", + // + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."String" = ANY (@strings) AND p."String" = ANY (@strings) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_of_strings_Contains_nullable_string(bool async) + { + await base.Parameter_collection_of_strings_Contains_nullable_string(async); + + AssertSql( + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableString" = ANY (@strings) OR (p."NullableString" IS NULL AND array_position(@strings, NULL) IS NOT NULL) +""", + // + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableString" = ANY (@strings) AND p."NullableString" = ANY (@strings) IS NOT NULL) AND (p."NullableString" IS NOT NULL OR array_position(@strings, NULL) IS NULL) +"""); + } + + public override async Task Parameter_collection_of_nullable_strings_Contains_string(bool async) + { + await base.Parameter_collection_of_nullable_strings_Contains_string(async); + + AssertSql( + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."String" = ANY (@strings) +""", + // + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."String" = ANY (@strings) AND p."String" = ANY (@strings) 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={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableString" = ANY (@strings) OR (p."NullableString" IS NULL AND array_position(@strings, NULL) IS NOT NULL) +""", + // + """ +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableString" = ANY (@strings) AND p."NullableString" = ANY (@strings) IS NOT NULL) AND (p."NullableString" IS NOT NULL OR array_position(@strings, NULL) IS NULL) +"""); + } + + public override async Task Parameter_collection_of_DateTimes_Contains(bool async) + { + await base.Parameter_collection_of_DateTimes_Contains(async); + + AssertSql( + """ +@dateTimes={ '2020-01-10T12:30:00.0000000Z', '9999-01-01T00:00:00.0000000Z' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."DateTime" = ANY (@dateTimes) +"""); + } + + public override async Task Parameter_collection_of_bools_Contains(bool async) + { + await base.Parameter_collection_of_bools_Contains(async); + + AssertSql( + """ +@bools={ 'True' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Bool" = ANY (@bools) +"""); + } + + public override async Task Parameter_collection_of_enums_Contains(bool async) + { + await base.Parameter_collection_of_enums_Contains(async); + + AssertSql( + """ +@enums={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Enum" = ANY (@enums) +"""); + } + + public override async Task Parameter_collection_null_Contains(bool async) + { + await base.Parameter_collection_null_Contains(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Int" = ANY (NULL) +"""); + } + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (2), (999), (1000)) AS i("Value") + WHERE i."Value" > p."Id") = 2 +"""); + } + + // The following test does nothing since we don't override NumberOfValuesForHugeParameterCollectionTests; + // the PG parameter limit is huge (ushort), so we don't need to test it. + public override async Task Parameter_collection_Count_with_huge_number_of_values(bool async) + { + await base.Parameter_collection_Count_with_huge_number_of_values(async); + + AssertSql(); + } + + // The following test does nothing since we don't override NumberOfValuesForHugeParameterCollectionTests; + // the PG parameter limit is huge (ushort), so we don't need to test it. + public override async Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values(bool async) + { + await base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values(async); + + AssertSql(); + } + + [ConditionalTheory] // #3012 + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in GaussDB 14 + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_of_ranges_Contains(bool async) + { + var ranges = new GaussDBRange[] + { + 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={ '[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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE @ranges @> p."Int" +"""); + } + + public override async Task Column_collection_of_ints_Contains(bool async) + { + await base.Column_collection_of_ints_Contains(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints" @> ARRAY[10]::integer[] +"""); + } + + public override async Task Column_collection_of_nullable_ints_Contains(bool async) + { + await base.Column_collection_of_nullable_ints_Contains(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableInts" @> ARRAY[10]::integer[] +"""); + } + + public override async Task Column_collection_of_nullable_ints_Contains_null(bool async) + { + await base.Column_collection_of_nullable_ints_Contains_null(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE array_position(p."NullableInts", NULL) IS NOT NULL +"""); + } + + public override async Task Column_collection_of_strings_contains_null(bool async) + { + await base.Column_collection_of_strings_contains_null(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE array_position(p."Strings", NULL) IS NOT NULL +"""); + } + + public override async Task Column_collection_of_nullable_strings_contains_null(bool async) + { + await base.Column_collection_of_nullable_strings_contains_null(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE array_position(p."NullableStrings", NULL) IS NOT NULL +"""); + } + + public override async Task Column_collection_of_bools_Contains(bool async) + { + await base.Column_collection_of_bools_Contains(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Bools" @> ARRAY[TRUE]::boolean[] +"""); + } + + public override async Task Column_collection_Count_method(bool async) + { + await base.Column_collection_Count_method(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(p."Ints") = 2 +"""); + } + + public override async Task Column_collection_Length(bool async) + { + await base.Column_collection_Length(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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); + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints"[2] = 10 +"""); + } + + public override async Task Column_collection_index_string(bool async) + { + await base.Column_collection_index_string(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Strings"[2] = '10' +"""); + } + + public override async Task Column_collection_index_datetime(bool async) + { + await base.Column_collection_index_datetime(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."DateTimes"[2] = TIMESTAMPTZ '2020-01-10T12:30:00Z' +"""); + } + + public override async Task Column_collection_index_beyond_end(bool async) + { + await base.Column_collection_index_beyond_end(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints"[1000] = 10 +"""); + } + + public override async Task Nullable_reference_column_collection_index_equals_nullable_column(bool async) + { + await base.Nullable_reference_column_collection_index_equals_nullable_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableStrings"[3] = p."NullableString" OR (p."NullableStrings"[3] IS NULL AND p."NullableString" IS NULL) +"""); + } + + public override async Task Non_nullable_reference_column_collection_index_equals_nullable_column(bool async) + { + await base.Non_nullable_reference_column_collection_index_equals_nullable_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(p."Strings") > 0 AND p."Strings"[2] = p."NullableString" +"""); + } + + public override async Task Inline_collection_index_Column(bool async) + { + await base.Inline_collection_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT v."Value" + FROM (VALUES (0, 1::int), (1, 2), (2, 3)) AS v(_ord, "Value") + ORDER BY v._ord NULLS FIRST + LIMIT 1 OFFSET p."Int") = 1 +"""); + } + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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); + + AssertSql( + """ +@ints={ '0', '2', '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE @ints[p."Int" + 1] = p."Int" +"""); + } + + public override async Task Parameter_collection_index_Column_equal_constant(bool async) + { + await base.Parameter_collection_index_Column_equal_constant(async); + + AssertSql( + """ +@ints={ '1', '2', '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE @ints[p."Int" + 1] = 1 +"""); + } + + public override async Task Column_collection_ElementAt(bool async) + { + await base.Column_collection_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints"[2] = 10 +"""); + } + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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); + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(p."Ints"[2:]) = 2 +"""); + } + + public override async Task Column_collection_Take(bool async) + { + await base.Column_collection_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE 11 = ANY (p."Ints"[:2]) +"""); + } + + public override async Task Column_collection_Skip_Take(bool async) + { + await base.Column_collection_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE 11 IN ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + WHERE i.value > 1 +) +"""); + } + + public override async Task Column_collection_OrderByDescending_ElementAt(bool async) + { + await base.Column_collection_OrderByDescending_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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + ORDER BY i.value DESC NULLS LAST + LIMIT 1 OFFSET 0) = 111 +"""); + } + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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); + + 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(p."Ints") > 0 +"""); + } + + public override async Task Column_collection_Distinct(bool async) + { + await base.Column_collection_Distinct(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT DISTINCT i.value + FROM unnest(p."Ints") AS i(value) + ) 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 +"""); + } + + public override async Task Column_collection_projection_from_top_level(bool async) + { + await base.Column_collection_projection_from_top_level(async); + + AssertSql( + """ +SELECT p."Ints" +FROM "PrimitiveCollectionsEntity" AS p +ORDER BY p."Id" NULLS FIRST +"""); + } + + public override async Task Column_collection_Join_parameter_collection(bool async) + { + await base.Column_collection_Join_parameter_collection(async); + + AssertSql( + """ +@ints={ '11', '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(p."Ints") AS i(value) + INNER JOIN unnest(@ints) AS i0(value) ON i.value = i0.value) = 2 +"""); + } + + public override async Task Inline_collection_Join_ordered_column_collection(bool async) + { + await base.Inline_collection_Join_ordered_column_collection(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (11::int), (111)) AS v("Value") + INNER JOIN unnest(p."Ints") AS i(value) ON v."Value" = i.value) = 2 +"""); + } + + public override async Task Parameter_collection_Concat_column_collection(bool async) + { + await base.Parameter_collection_Concat_column_collection(async); + + AssertSql( + """ +@ints={ '11', '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(@ints || 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={ 'one', 'two' } (DbType = Object) + +SELECT CASE + WHEN p."Id" <> 0 THEN @values[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); + + AssertSql( + """ +@ints={ '11', '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + UNION + SELECT i0.value + FROM unnest(@ints) AS i0(value) + ) AS u) = 2 +"""); + } + + public override async Task Column_collection_Intersect_inline_collection(bool async) + { + await base.Column_collection_Intersect_inline_collection(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + INTERSECT + VALUES (11::int), (111) + ) AS i0) = 2 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Column_collection_Intersect_Parameter_collection_Any(bool async) + { + var ints = new[] { 11, 12 }; + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints.Intersect(ints).Any())); + + AssertSql( + """ +@ints={ '11', '12' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints" && @ints +"""); + } + + public override async Task Inline_collection_Except_column_collection(bool async) + { + await base.Inline_collection_Except_column_collection(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT v."Value" + FROM (VALUES (11::int), (111)) AS v("Value") + EXCEPT + SELECT i.value AS "Value" + FROM unnest(p."Ints") AS i(value) + ) 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_Concat_Column_collection_Concat_parameter(bool async) + { + var ints1 = new[] { 11 }; + var ints2 = new[] { 12 }; + + await AssertQuery( + async, + ss => ss.Set().Where(c => ints1.Concat(c.Ints).Concat(ints2).Count() == 4)); + + AssertSql( + """ +@ints1={ '11' } (DbType = Object) +@ints2={ '12' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(@ints1 || p."Ints" || @ints2) = 4 +"""); + } + + public override async Task Column_collection_Concat_parameter_collection_equality_inline_collection(bool async) + { + await base.Column_collection_Concat_parameter_collection_equality_inline_collection(async); + + AssertSql(); + } + + public override async Task Column_collection_equality_parameter_collection(bool async) + { + await base.Column_collection_equality_parameter_collection(async); + + AssertSql( + """ +@ints={ '1', '10' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints" = @ints +"""); + } + + public override async Task Column_collection_equality_inline_collection(bool async) + { + await base.Column_collection_equality_inline_collection(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints" = ARRAY[1,10]::integer[] +"""); + } + + public override async Task Column_collection_equality_inline_collection_with_parameters(bool async) + { + var (i, j) = (1, 10); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.Ints == new[] { i, j }), + ss => ss.Set().Where(c => c.Ints.SequenceEqual(new[] { i, j }))); + + AssertSql( + """ +@i='1' +@j='10' + +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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Ints" = ARRAY[@i,@j]::integer[] +"""); + } + + 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); + + AssertSql( + """ +@ints={ '10', '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT i.value + FROM unnest(@ints[2:]) AS i(value) + UNION + SELECT i0.value + FROM unnest(p."Ints") AS i0(value) + ) AS u) = 3 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection(async); + + AssertSql( + """ +@Skip={ '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT s.value + FROM unnest(@Skip) AS s(value) + UNION + SELECT i.value + FROM unnest(p."Ints") AS i(value) + ) AS u) = 3 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_nested(async); + + AssertSql( + """ +@Skip={ '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT s.value + FROM unnest(@Skip) AS s(value) + UNION + SELECT i2.value + FROM ( + SELECT i1.value + FROM ( + SELECT DISTINCT i0.value + FROM ( + SELECT i.value + FROM unnest(p."Ints") AS i(value) + ORDER BY i.value NULLS FIRST + OFFSET 1 + ) AS i0 + ) AS i1 + ORDER BY i1.value DESC NULLS LAST + LIMIT 20 + ) AS i2 + ) AS u) = 3 +"""); + } + + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() + { + base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); + + AssertSql(); + } + + public override async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(async); + + AssertSql(); + } + + public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async) + { + await base.Parameter_collection_in_subquery_Count_as_compiled_query(async); + + AssertSql( + """ +@ints={ '10', '111' } (DbType = Object) + +SELECT count(*)::int +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM unnest(@ints[2:]) AS i(value) + WHERE i.value > p."Id") = 1 +"""); + } + + public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async) + { + await base.Column_collection_in_subquery_Union_parameter_collection(async); + + AssertSql( + """ +@ints={ '10', '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM ( + SELECT i.value + FROM unnest(p."Ints"[2:]) AS i(value) + UNION + SELECT i0.value + FROM unnest(@ints) AS i0(value) + ) AS u) = 3 +"""); + } + + public override async Task Project_collection_of_ints_simple(bool async) + { + await base.Project_collection_of_ints_simple(async); + + AssertSql( + """ +SELECT p."Ints" +FROM "PrimitiveCollectionsEntity" AS p +ORDER BY p."Id" NULLS FIRST +"""); + } + + public override async Task Project_collection_of_ints_ordered(bool async) + { + await base.Project_collection_of_ints_ordered(async); + + AssertSql( + """ +SELECT p."Id", i.value, i.ordinality +FROM "PrimitiveCollectionsEntity" AS p +LEFT JOIN LATERAL unnest(p."Ints") WITH ORDINALITY AS i(value) ON TRUE +ORDER BY p."Id" NULLS FIRST, i.value DESC NULLS LAST +"""); + } + + public override async Task Project_collection_of_datetimes_filtered(bool async) + { + await base.Project_collection_of_datetimes_filtered(async); + + AssertSql( + """ +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 +) AS d0 ON TRUE +ORDER BY p."Id" NULLS FIRST, d0.ordinality NULLS FIRST +"""); + } + + public override async Task Project_collection_of_nullable_ints_with_paging(bool async) + { + await base.Project_collection_of_nullable_ints_with_paging(async); + + AssertSql( + """ +SELECT p."Id", n.value, n.ordinality +FROM "PrimitiveCollectionsEntity" AS p +LEFT JOIN LATERAL unnest(p."NullableInts"[:20]) WITH ORDINALITY AS n(value) ON TRUE +ORDER BY p."Id" NULLS FIRST +"""); + } + + public override async Task Project_collection_of_nullable_ints_with_paging2(bool async) + { + await base.Project_collection_of_nullable_ints_with_paging2(async); + + AssertSql( + """ +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 n0 ON TRUE +ORDER BY p."Id" NULLS FIRST, n0.value NULLS FIRST +"""); + } + + public override async Task Project_collection_of_nullable_ints_with_paging3(bool async) + { + await base.Project_collection_of_nullable_ints_with_paging3(async); + + AssertSql( + """ +SELECT p."Id", n.value, n.ordinality +FROM "PrimitiveCollectionsEntity" AS p +LEFT JOIN LATERAL unnest(p."NullableInts"[3:]) WITH ORDINALITY AS n(value) ON TRUE +ORDER BY p."Id" NULLS FIRST +"""); + } + + public override async Task Project_collection_of_ints_with_distinct(bool async) + { + await base.Project_collection_of_ints_with_distinct(async); + + AssertSql( + """ +SELECT p."Id", i0.value +FROM "PrimitiveCollectionsEntity" AS p +LEFT JOIN LATERAL ( + SELECT DISTINCT i.value + FROM unnest(p."Ints") AS i(value) +) AS i0 ON TRUE +ORDER BY p."Id" NULLS FIRST +"""); + } + + public override async Task Project_collection_of_nullable_ints_with_distinct(bool async) + { + await base.Project_collection_of_nullable_ints_with_distinct(async); + + 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", 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 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 n2 ON TRUE +ORDER BY p."Id" NULLS FIRST, n1.ordinality NULLS FIRST, n2.ordinality NULLS FIRST +"""); + } + + public override async Task Project_multiple_collections(bool async) + { + // Base implementation currently uses an Unspecified DateTime in the query, but we require a Utc one. + await AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select( + x => new + { + Ints = x.Ints.ToList(), + OrderedInts = x.Ints.OrderByDescending(xx => xx).ToList(), + FilteredDateTimes = x.DateTimes.Where(xx => xx.Day != 1).ToList(), + FilteredDateTimes2 = x.DateTimes.Where(xx => xx > new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ToList() + }), + elementAsserter: (e, a) => + { + AssertCollection(e.Ints, a.Ints, ordered: true); + AssertCollection(e.OrderedInts, a.OrderedInts, ordered: true); + AssertCollection(e.FilteredDateTimes, a.FilteredDateTimes, elementSorter: ee => ee); + AssertCollection(e.FilteredDateTimes2, a.FilteredDateTimes2, elementSorter: ee => ee); + }, + assertOrder: true); + + AssertSql( + """ +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 +) 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 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 +"""); + } + + public override async Task Project_primitive_collections_element(bool async) + { + await base.Project_primitive_collections_element(async); + + AssertSql( + """ +SELECT p."Ints"[1] AS "Indexer", p."DateTimes"[1] AS "EnumerableElementAt", p."Strings"[2] AS "QueryableElementAt" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."Id" < 4 +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={ '1', '2', '3' } (DbType = Object) +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE CASE + WHEN p."Int" = ANY (@ints) THEN 'one' + ELSE 'two' +END = ANY (@strings) +"""); + } + + 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={ '1', '2', '3' } (DbType = Object) +@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE CASE + WHEN p."Int" = ANY (@ints) THEN 'one' + ELSE 'two' +END = ANY (@strings) +"""); + } + + public override async Task Values_of_enum_casted_to_underlying_value(bool async) + { + await base.Values_of_enum_casted_to_underlying_value(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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE ( + SELECT count(*)::int + FROM (VALUES (0::int), (1), (2), (3)) AS v("Value") + WHERE v."Value" = p."Int") > 0 +"""); + } + + [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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE cardinality(array_remove(p."Ints", 1)) = 1 +"""); + } + + public override async Task Parameter_collection_of_structs_Contains_nullable_struct(bool async) + { + await base.Parameter_collection_of_structs_Contains_nullable_struct(async); + + AssertSql( + """ +@values={ '22', '33' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableWrappedId" = ANY (@values) OR (p."NullableWrappedId" IS NULL AND array_position(@values, NULL) IS NOT NULL) +""", + // + """ +@values={ '11', '44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableWrappedId" = ANY (@values) AND p."NullableWrappedId" = ANY (@values) IS NOT NULL) AND (p."NullableWrappedId" IS NOT NULL OR array_position(@values, NULL) IS NULL) +"""); + } + + public override Task Parameter_collection_of_structs_Contains_nullable_struct_with_nullable_comparer(bool async) + => Assert.ThrowsAnyAsync( + () => base.Parameter_collection_of_structs_Contains_nullable_struct_with_nullable_comparer(async)); + + public override async Task Parameter_collection_of_nullable_structs_Contains_struct(bool async) + { + await base.Parameter_collection_of_nullable_structs_Contains_struct(async); + + AssertSql( + """ +@values={ NULL, '22' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."WrappedId" = ANY (@values) +""", + // + """ +@values={ '11', '44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."WrappedId" = ANY (@values) AND p."WrappedId" = ANY (@values) IS NOT NULL) +"""); + } + + public override async Task Parameter_collection_of_nullable_structs_Contains_nullable_struct(bool async) + { + await base.Parameter_collection_of_nullable_structs_Contains_nullable_struct(async); + + AssertSql( + """ +@values={ NULL, '22' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE p."NullableWrappedId" = ANY (@values) OR (p."NullableWrappedId" IS NULL AND array_position(@values, NULL) IS NOT NULL) +""", + // + """ +@values={ '11', '44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" +FROM "PrimitiveCollectionsEntity" AS p +WHERE NOT (p."NullableWrappedId" = ANY (@values) AND p."NullableWrappedId" = ANY (@values) IS NOT NULL) AND (p."NullableWrappedId" IS NOT NULL OR array_position(@values, NULL) IS NULL) +"""); + } + + public override Task Parameter_collection_of_nullable_structs_Contains_nullable_struct_with_nullable_comparer(bool async) + => Assert.ThrowsAnyAsync( + () => base.Parameter_collection_of_nullable_structs_Contains_nullable_struct_with_nullable_comparer(async)); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private PrimitiveCollectionsContext CreateContext() + => Fixture.CreateContext(); + + public class PrimitiveCollectionsQueryGaussDBFixture : PrimitiveCollectionsQueryFixtureBase, ITestSqlLoggerFactory + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/QueryBugTest.cs similarity index 81% rename from test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/QueryBugTest.cs index 9d5aa20425..243c629919 100644 --- a/test/EFCore.PG.FunctionalTests/Query/QueryBugTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/QueryBugTest.cs @@ -6,17 +6,17 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public class QueryBugsTest : IClassFixture +public class QueryBugsTest : IClassFixture { // ReSharper disable once UnusedParameter.Local - public QueryBugsTest(NpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public QueryBugsTest(GaussDBFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - protected NpgsqlFixture Fixture { get; } + protected GaussDBFixture Fixture { get; } #region Bug920 @@ -29,7 +29,7 @@ public async Task Bug920() context.SaveChanges(); } - private Task CreateDatabase920Async() + private Task CreateDatabase920Async() => CreateTestStoreAsync(() => new Bug920Context(_options), _ => ClearLog()); public enum Bug920Enum { One, Two } @@ -51,12 +51,12 @@ private class Bug920Context(DbContextOptions options) : DbContext(options) private DbContextOptions _options; - private async Task CreateTestStoreAsync( + private async Task CreateTestStoreAsync( Func contextCreator, Action contextInitializer) where TContext : DbContext, IDisposable { - var testStore = await NpgsqlTestStore.CreateInitializedAsync("QueryBugsTest"); + var testStore = await GaussDBTestStore.CreateInitializedAsync("QueryBugsTest"); _options = Fixture.CreateOptions(testStore); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/QueryFilterFuncletizationGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/QueryFilterFuncletizationGaussDBTest.cs new file mode 100644 index 0000000000..15a2ca3005 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/QueryFilterFuncletizationGaussDBTest.cs @@ -0,0 +1,41 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class QueryFilterFuncletizationGaussDBTest + : QueryFilterFuncletizationTestBase +{ + // ReSharper disable once UnusedParameter.Local + public QueryFilterFuncletizationGaussDBTest( + QueryFilterFuncletizationGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public override void DbContext_list_is_parameterized() + { + using var context = CreateContext(); + + // The standard EF Core implementation expands the query filter-referenced list client side, and so generates an NRE + // when the list is null. We translate to server-side with PostgresAnyExpression, so no exception is thrown. + // Assert.Throws(() => context.Set().ToList()); + + context.TenantIds = []; + var query = context.Set().ToList(); + Assert.Empty(query); + + context.TenantIds = [1]; + query = context.Set().ToList(); + Assert.Single(query); + + context.TenantIds = [2, 3]; + query = context.Set().ToList(); + Assert.Equal(2, query.Count); + } + + public class QueryFilterFuncletizationGaussDBFixture : QueryFilterFuncletizationRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/QueryNoClientEvalGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/QueryNoClientEvalGaussDBFixture.cs new file mode 100644 index 0000000000..10d88d101a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/QueryNoClientEvalGaussDBFixture.cs @@ -0,0 +1,3 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class QueryNoClientEvalGaussDBFixture : NorthwindQueryGaussDBFixture; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/QueryNoClientEvalGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/QueryNoClientEvalGaussDBTest.cs new file mode 100644 index 0000000000..80a2c164d5 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/QueryNoClientEvalGaussDBTest.cs @@ -0,0 +1,4 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class QueryNoClientEvalGaussDBTest(QueryNoClientEvalGaussDBFixture fixture) + : QueryNoClientEvalTestBase(fixture); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/ComplexRelationshipsGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/ComplexRelationshipsGaussDBFixture.cs new file mode 100644 index 0000000000..b62e243be8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/ComplexRelationshipsGaussDBFixture.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public class ComplexRelationshipsGaussDBFixture : ComplexRelationshipsRelationalFixtureBase, ITestSqlLoggerFactory +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Include/NavigationIncludeGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Include/NavigationIncludeGaussDBTest.cs new file mode 100644 index 0000000000..3c3baf65b0 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Include/NavigationIncludeGaussDBTest.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Include; + +public class NavigationIncludeGaussDBTest + : NavigationIncludeRelationalTestBase +{ + public NavigationIncludeGaussDBTest(NavigationRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Include_trunk_required(bool async) + { + await base.Include_trunk_required(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +"""); + } + + public override async Task Include_trunk_optional(bool async) + { + await base.Include_trunk_optional(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +"""); + } + + public override async Task Include_trunk_collection(bool async) + { + await base.Include_trunk_collection(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."Id" = t."CollectionRootId" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Include_trunk_required_optional_and_collection(bool async) + { + await base.Include_trunk_required_optional_and_collection(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t0."Id", t0."CollectionRootId", t0."Name", t0."OptionalReferenceBranchId", t0."RequiredReferenceBranchId", t1."Id", t1."CollectionRootId", t1."Name", t1."OptionalReferenceBranchId", t1."RequiredReferenceBranchId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "TrunkEntities" AS t0 ON r."OptionalReferenceTrunkId" = t0."Id" +LEFT JOIN "TrunkEntities" AS t1 ON r."Id" = t1."CollectionRootId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, t0."Id" NULLS FIRST +"""); + } + + public override async Task Include_branch_required_required(bool async) + { + await base.Include_branch_required_required(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +"""); + } + + public override async Task Include_branch_required_collection(bool async) + { + await base.Include_branch_required_collection(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST +"""); + } + + public override async Task Include_branch_optional_optional(bool async) + { + await base.Include_branch_optional_optional(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +"""); + } + + public override async Task Include_branch_optional_collection(bool async) + { + await base.Include_branch_optional_collection(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST +"""); + } + + public override async Task Include_branch_collection_collection(bool async) + { + await base.Include_branch_collection_collection(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", s."Id", s."CollectionRootId", s."Name", s."OptionalReferenceBranchId", s."RequiredReferenceBranchId", s."Id0", s."CollectionTrunkId", s."Name0", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN ( + SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id" AS "Id0", b."CollectionTrunkId", b."Name" AS "Name0", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" + FROM "TrunkEntities" AS t + LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +) AS s ON r."Id" = s."CollectionRootId" +ORDER BY r."Id" NULLS FIRST, s."Id" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/NavigationRelationshipsGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/NavigationRelationshipsGaussDBFixture.cs new file mode 100644 index 0000000000..e260679057 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/NavigationRelationshipsGaussDBFixture.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public class NavigationRelationshipsGaussDBFixture : NavigationRelationshipsRelationalFixtureBase, ITestSqlLoggerFactory +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedJsonRelationshipsGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedJsonRelationshipsGaussDBFixture.cs new file mode 100644 index 0000000000..bbfee0147a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedJsonRelationshipsGaussDBFixture.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public class OwnedJsonRelationshipsGaussDBFixture : OwnedJsonRelationshipsRelationalFixtureBase, ITestSqlLoggerFactory +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedJsonTypeRelationshipsGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedJsonTypeRelationshipsGaussDBFixture.cs new file mode 100644 index 0000000000..5b9c9b86d1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedJsonTypeRelationshipsGaussDBFixture.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.TestModels.RelationshipsModel; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public class OwnedJsonTypeRelationshipsGaussDBFixture : OwnedJsonRelationshipsRelationalFixtureBase, ITestSqlLoggerFactory +{ + protected override string StoreName => "JsonTypeRelationshipsQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().OwnsOne(x => x.RequiredReferenceTrunk).HasColumnType("json"); + modelBuilder.Entity().OwnsOne(x => x.OptionalReferenceTrunk).HasColumnType("json"); + modelBuilder.Entity().OwnsMany(x => x.CollectionTrunk).HasColumnType("json"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedRelationshipsGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedRelationshipsGaussDBFixture.cs new file mode 100644 index 0000000000..9eba52fa49 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedRelationshipsGaussDBFixture.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public class OwnedRelationshipsGaussDBFixture : OwnedRelationshipsRelationalFixtureBase, ITestSqlLoggerFactory +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedTableSplittingRelationshipsGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedTableSplittingRelationshipsGaussDBFixture.cs new file mode 100644 index 0000000000..818e4f621a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/OwnedTableSplittingRelationshipsGaussDBFixture.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public class OwnedTableSplittingRelationshipsGaussDBFixture : OwnedTableSplittingRelationshipsRelationalFixtureBase, ITestSqlLoggerFactory +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/ComplexNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/ComplexNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..8246c0170d --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/ComplexNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class ComplexNoTrackingProjectionGaussDBTest + : ComplexNoTrackingProjectionRelationalTestBase +{ + public ComplexNoTrackingProjectionGaussDBTest(ComplexRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_everything(bool async) + { + await base.Select_everything(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."Id" +"""); + } + + public override async Task Select_root(bool async) + { + await base.Select_root(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_optional(bool async) + { + await base.Select_trunk_optional(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_required(bool async) + { + await base.Select_trunk_required(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql(); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql(); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql(); + } + + public override async Task Select_root_duplicated(bool async) + { + await base.Select_root_duplicated(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_and_branch_duplicated(bool async) + { + await base.Select_trunk_and_branch_duplicated(async); + + AssertSql(); + } + + public override async Task Select_trunk_and_trunk_duplicated(bool async) + { + await base.Select_trunk_and_trunk_duplicated(async); + + AssertSql(); + } + + public override async Task Select_leaf_trunk_root(bool async) + { + await base.Select_leaf_trunk_root(async); + + AssertSql( + """ +SELECT t."RequiredReferenceBranch_RequiredReferenceLeaf_Name", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +"""); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async); + + AssertSql(); + } + + public override async Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async); + + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/ComplexProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/ComplexProjectionGaussDBTest.cs new file mode 100644 index 0000000000..b7a91e8350 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/ComplexProjectionGaussDBTest.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class ComplexProjectionGaussDBTest + : ComplexProjectionRelationalTestBase +{ + public ComplexProjectionGaussDBTest(ComplexRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_everything(bool async) + { + await base.Select_everything(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."Id" +"""); + } + + public override async Task Select_root(bool async) + { + await base.Select_root(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_optional(bool async) + { + await base.Select_trunk_optional(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_required(bool async) + { + await base.Select_trunk_required(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT t."RequiredReferenceBranch_Name", t."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql(); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql(); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql(); + } + + public override async Task Select_root_duplicated(bool async) + { + await base.Select_root_duplicated(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_and_branch_duplicated(bool async) + { + await base.Select_trunk_and_branch_duplicated(async); + + AssertSql(); + } + + public override async Task Select_trunk_and_trunk_duplicated(bool async) + { + await base.Select_trunk_and_trunk_duplicated(async); + + AssertSql(); + } + + public override async Task Select_leaf_trunk_root(bool async) + { + await base.Select_leaf_trunk_root(async); + + AssertSql( + """ +SELECT t."RequiredReferenceBranch_RequiredReferenceLeaf_Name", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", t."RequiredReferenceBranch_Name", r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +"""); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async); + + AssertSql(); + } + + public override async Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async); + + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..f55a9455a2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class NavigationNoTrackingProjectionGaussDBTest + : NavigationNoTrackingProjectionRelationalTestBase +{ + public NavigationNoTrackingProjectionGaussDBTest(NavigationRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_everything_using_joins(bool async) + { + await base.Select_everything_using_joins(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", l."Id", l."CollectionBranchId", l."Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."Id" +INNER JOIN "LeafEntities" AS l ON b."Id" = l."Id" +"""); + } + + public override async Task Select_trunk_collection(bool async) + { + await base.Select_trunk_collection(async); + AssertSql( + """ +SELECT r."Id", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."Id" = t."CollectionRootId" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_collection(bool async) + { + await base.Select_branch_required_collection(async); + + AssertSql( + """ +SELECT r."Id", t."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_collection(bool async) + { + await base.Select_branch_optional_collection(async); + + AssertSql( + """ +SELECT r."Id", t."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST +"""); + } + + public override async Task Select_multiple_branch_leaf(bool async) + { + await base.Select_multiple_branch_leaf(async); + + AssertSql( + """ +SELECT r."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", l."Id", l."CollectionBranchId", l."Name", t."Id", l0."Id", l0."CollectionBranchId", l0."Name", b0."Id", b0."CollectionTrunkId", b0."Name", b0."OptionalReferenceLeafId", b0."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +LEFT JOIN "LeafEntities" AS l ON b."OptionalReferenceLeafId" = l."Id" +LEFT JOIN "LeafEntities" AS l0 ON b."Id" = l0."CollectionBranchId" +LEFT JOIN "BranchEntities" AS b0 ON t."Id" = b0."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, b."Id" NULLS FIRST, l."Id" NULLS FIRST, l0."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + { + await base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async); + + AssertSql( + """ +SELECT r."Id", s."Id", s."Id0", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", s.c +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT 1 AS c, r0."Id", t."Id" AS "Id0" + FROM "RootEntities" AS r0 + INNER JOIN "TrunkEntities" AS t ON r0."RequiredReferenceTrunkId" = t."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +LEFT JOIN "BranchEntities" AS b ON s."Id0" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + { + await base.Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(async); + + AssertSql( + """ +SELECT r."Id", t."Id", b."Id", s."Id1", s."Id", s."Id0", b1."Id", b1."CollectionTrunkId", b1."Name", b1."OptionalReferenceLeafId", b1."RequiredReferenceLeafId", s."CollectionRootId", s."Name", s."OptionalReferenceBranchId", s."RequiredReferenceBranchId", s."CollectionTrunkId", s."Name0", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId", s."Name1", s.c +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +LEFT JOIN LATERAL ( + SELECT t0."Id", t0."CollectionRootId", t0."Name", t0."OptionalReferenceBranchId", t0."RequiredReferenceBranchId", b0."Id" AS "Id0", b0."CollectionTrunkId", b0."Name" AS "Name0", b0."OptionalReferenceLeafId", b0."RequiredReferenceLeafId", b."Name" AS "Name1", 1 AS c, r0."Id" AS "Id1" + FROM "RootEntities" AS r0 + INNER JOIN "TrunkEntities" AS t0 ON r0."RequiredReferenceTrunkId" = t0."Id" + INNER JOIN "BranchEntities" AS b0 ON t0."RequiredReferenceBranchId" = b0."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +LEFT JOIN "BranchEntities" AS b1 ON t."Id" = b1."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, b."Id" NULLS FIRST, s."Id1" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + { + await base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async); + + AssertSql( + """ +SELECT r."Id", t."Id", r1."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", r1.c +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN LATERAL ( + SELECT 1 AS c, r0."Id" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, r1."Id" NULLS FIRST +"""); + } + + public override async Task SelectMany_trunk_collection(bool async) + { + await base.SelectMany_trunk_collection(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."CollectionRootId" +"""); + } + + public override async Task SelectMany_required_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_required_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +"""); + } + + public override async Task SelectMany_optional_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_optional_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationProjectionGaussDBTest.cs new file mode 100644 index 0000000000..cccf5548c9 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationProjectionGaussDBTest.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class NavigationProjectionGaussDBTest + : NavigationProjectionRelationalTestBase +{ + public NavigationProjectionGaussDBTest(NavigationRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_everything_using_joins(bool async) + { + await base.Select_everything_using_joins(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", l."Id", l."CollectionBranchId", l."Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."Id" +INNER JOIN "LeafEntities" AS l ON b."Id" = l."Id" +"""); + } + + public override async Task Select_trunk_collection(bool async) + { + await base.Select_trunk_collection(async); + + AssertSql( + """ +SELECT r."Id", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."Id" = t."CollectionRootId" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_collection(bool async) + { + await base.Select_branch_required_collection(async); + + AssertSql( + """ +SELECT r."Id", t."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_collection(bool async) + { + await base.Select_branch_optional_collection(async); + + AssertSql( + """ +SELECT r."Id", t."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST +"""); + } + + public override async Task Select_multiple_branch_leaf(bool async) + { + await base.Select_multiple_branch_leaf(async); + + AssertSql( + """ +SELECT r."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", l."Id", l."CollectionBranchId", l."Name", t."Id", l0."Id", l0."CollectionBranchId", l0."Name", b0."Id", b0."CollectionTrunkId", b0."Name", b0."OptionalReferenceLeafId", b0."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +LEFT JOIN "LeafEntities" AS l ON b."OptionalReferenceLeafId" = l."Id" +LEFT JOIN "LeafEntities" AS l0 ON b."Id" = l0."CollectionBranchId" +LEFT JOIN "BranchEntities" AS b0 ON t."Id" = b0."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, b."Id" NULLS FIRST, l."Id" NULLS FIRST, l0."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + { + await base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async); + + AssertSql( + """ +SELECT r."Id", s."Id", s."Id0", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", s.c +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT 1 AS c, r0."Id", t."Id" AS "Id0" + FROM "RootEntities" AS r0 + INNER JOIN "TrunkEntities" AS t ON r0."RequiredReferenceTrunkId" = t."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +LEFT JOIN "BranchEntities" AS b ON s."Id0" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + { + await base.Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(async); + + AssertSql( + """ +SELECT r."Id", t."Id", b."Id", s."Id1", s."Id", s."Id0", b1."Id", b1."CollectionTrunkId", b1."Name", b1."OptionalReferenceLeafId", b1."RequiredReferenceLeafId", s."CollectionRootId", s."Name", s."OptionalReferenceBranchId", s."RequiredReferenceBranchId", s."CollectionTrunkId", s."Name0", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId", s."Name1", s.c +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +LEFT JOIN LATERAL ( + SELECT t0."Id", t0."CollectionRootId", t0."Name", t0."OptionalReferenceBranchId", t0."RequiredReferenceBranchId", b0."Id" AS "Id0", b0."CollectionTrunkId", b0."Name" AS "Name0", b0."OptionalReferenceLeafId", b0."RequiredReferenceLeafId", b."Name" AS "Name1", 1 AS c, r0."Id" AS "Id1" + FROM "RootEntities" AS r0 + INNER JOIN "TrunkEntities" AS t0 ON r0."RequiredReferenceTrunkId" = t0."Id" + INNER JOIN "BranchEntities" AS b0 ON t0."RequiredReferenceBranchId" = b0."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +LEFT JOIN "BranchEntities" AS b1 ON t."Id" = b1."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, b."Id" NULLS FIRST, s."Id1" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + { + await base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async); + + AssertSql( + """ +SELECT r."Id", t."Id", r1."Id", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", r1.c +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN LATERAL ( + SELECT 1 AS c, r0."Id" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +LEFT JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +ORDER BY r."Id" NULLS FIRST, t."Id" NULLS FIRST, r1."Id" NULLS FIRST +"""); + } + + public override async Task SelectMany_trunk_collection(bool async) + { + await base.SelectMany_trunk_collection(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."CollectionRootId" +"""); + } + + public override async Task SelectMany_required_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_required_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +"""); + } + + public override async Task SelectMany_optional_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_optional_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."CollectionTrunkId" +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationReferenceNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationReferenceNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..013d4a7d86 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationReferenceNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class NavigationReferenceNoTrackingProjectionSqlServerTest + : NavigationReferenceNoTrackingProjectionRelationalTestBase +{ + public NavigationReferenceNoTrackingProjectionSqlServerTest(NavigationRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_everything(bool async) + { + await base.Select_everything(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", l."Id", l."CollectionBranchId", l."Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."Id" +INNER JOIN "LeafEntities" AS l ON b."Id" = l."Id" +"""); + } + + public override async Task Select_root(bool async) + { + await base.Select_root(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_optional(bool async) + { + await base.Select_trunk_optional(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_required(bool async) + { + await base.Select_trunk_required(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_root_duplicated(bool async) + { + await base.Select_root_duplicated(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_and_branch_duplicated(bool async) + { + await base.Select_trunk_and_branch_duplicated(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_and_trunk_duplicated(bool async) + { + await base.Select_trunk_and_trunk_duplicated(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", l."Id", l."CollectionBranchId", l."Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +LEFT JOIN "LeafEntities" AS l ON b."RequiredReferenceLeafId" = l."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_leaf_trunk_root(bool async) + { + await base.Select_leaf_trunk_root(async); + + AssertSql( + """ +SELECT l."Id", l."CollectionBranchId", l."Name", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +INNER JOIN "LeafEntities" AS l ON b."RequiredReferenceLeafId" = l."Id" +"""); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT s."Id", s."CollectionTrunkId", s."Name", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" + FROM "RootEntities" AS r0 + INNER JOIN "TrunkEntities" AS t ON r0."RequiredReferenceTrunkId" = t."Id" + INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT s."Id", s."CollectionTrunkId", s."Name", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" + FROM "RootEntities" AS r0 + LEFT JOIN "TrunkEntities" AS t ON r0."OptionalReferenceTrunkId" = t."Id" + LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationReferenceProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationReferenceProjectionGaussDBTest.cs new file mode 100644 index 0000000000..8e40f4c01f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/NavigationReferenceProjectionGaussDBTest.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class NavigationReferenceProjectionGaussDBTest + : NavigationReferenceProjectionRelationalTestBase +{ + public NavigationReferenceProjectionGaussDBTest(NavigationRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_everything(bool async) + { + await base.Select_everything(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId", l."Id", l."CollectionBranchId", l."Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."Id" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."Id" = b."Id" +INNER JOIN "LeafEntities" AS l ON b."Id" = l."Id" +"""); + } + + public override async Task Select_root(bool async) + { + await base.Select_root(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_optional(bool async) + { + await base.Select_trunk_optional(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_required(bool async) + { + await base.Select_trunk_required(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql( + """ +SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_root_duplicated(bool async) + { + await base.Select_root_duplicated(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_and_branch_duplicated(bool async) + { + await base.Select_trunk_and_branch_duplicated(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN "TrunkEntities" AS t ON r."OptionalReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_and_trunk_duplicated(bool async) + { + await base.Select_trunk_and_trunk_duplicated(async); + + AssertSql( + """ +SELECT t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", l."Id", l."CollectionBranchId", l."Name" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" +LEFT JOIN "LeafEntities" AS l ON b."RequiredReferenceLeafId" = l."Id" +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_leaf_trunk_root(bool async) + { + await base.Select_leaf_trunk_root(async); + + AssertSql( + """ +SELECT l."Id", l."CollectionBranchId", l."Name", t."Id", t."CollectionRootId", t."Name", t."OptionalReferenceBranchId", t."RequiredReferenceBranchId", r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId" +FROM "RootEntities" AS r +INNER JOIN "TrunkEntities" AS t ON r."RequiredReferenceTrunkId" = t."Id" +INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" +INNER JOIN "LeafEntities" AS l ON b."RequiredReferenceLeafId" = l."Id" +"""); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT s."Id", s."CollectionTrunkId", s."Name", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" + FROM "RootEntities" AS r0 + INNER JOIN "TrunkEntities" AS t ON r0."RequiredReferenceTrunkId" = t."Id" + INNER JOIN "BranchEntities" AS b ON t."RequiredReferenceBranchId" = b."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT s."Id", s."CollectionTrunkId", s."Name", s."OptionalReferenceLeafId", s."RequiredReferenceLeafId" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT b."Id", b."CollectionTrunkId", b."Name", b."OptionalReferenceLeafId", b."RequiredReferenceLeafId" + FROM "RootEntities" AS r0 + LEFT JOIN "TrunkEntities" AS t ON r0."OptionalReferenceTrunkId" = t."Id" + LEFT JOIN "BranchEntities" AS b ON t."OptionalReferenceBranchId" = b."Id" + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS s ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..866241947c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedJsonNoTrackingProjectionGaussDBTest + : OwnedJsonNoTrackingProjectionRelationalTestBase +{ + public OwnedJsonNoTrackingProjectionGaussDBTest(OwnedJsonRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_trunk_collection(bool async) + { + await base.Select_trunk_collection(async); + + AssertSql( + """ +SELECT r."CollectionTrunk", r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_collection(bool async) + { + await base.Select_branch_required_collection(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'CollectionBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_collection(bool async) + { + await base.Select_branch_optional_collection(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'CollectionBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_multiple_branch_leaf(bool async) + { + await base.Select_multiple_branch_leaf(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk" -> 'RequiredReferenceBranch', r."RequiredReferenceTrunk" #> '{RequiredReferenceBranch,OptionalReferenceLeaf}', r."RequiredReferenceTrunk" #> '{RequiredReferenceBranch,CollectionLeaf}', r."RequiredReferenceTrunk" -> 'CollectionBranch', r."RequiredReferenceTrunk" #>> '{RequiredReferenceBranch,OptionalReferenceLeaf,Name}' +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + { + await base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async); + + AssertSql( + """ +SELECT r1.c, r1."Id", r1.c0 +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT r0."RequiredReferenceTrunk" -> 'CollectionBranch' AS c, r0."Id", 1 AS c0 + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + { + await base.Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(async); + + AssertSql( + """ +SELECT r1.c, r1."Id", r1.c0, r1."Id0", r1.c1, r1.c2, r1.c3, r1.c4 +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT r."RequiredReferenceTrunk" -> 'CollectionBranch' AS c, r."Id", r0."RequiredReferenceTrunk" AS c0, r0."Id" AS "Id0", r0."RequiredReferenceTrunk" -> 'RequiredReferenceBranch' AS c1, r0."RequiredReferenceTrunk" ->> 'Name' AS c2, r."RequiredReferenceTrunk" #>> '{RequiredReferenceBranch,Name}' AS c3, 1 AS c4 + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + { + await base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async); + + AssertSql( + """ +SELECT r1.c, r1."Id", r1.c0 +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT r."RequiredReferenceTrunk" -> 'CollectionBranch' AS c, r."Id", 1 AS c0 + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task SelectMany_trunk_collection(bool async) + { + await base.SelectMany_trunk_collection(async); + + AssertSql( + """ +SELECT r."Id", c."Name", c."CollectionBranch", c."OptionalReferenceBranch", c."RequiredReferenceBranch" +FROM "RootEntities" AS r +JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."CollectionTrunk") AS ( + "Name" text, + "CollectionBranch" jsonb, + "OptionalReferenceBranch" jsonb, + "RequiredReferenceBranch" jsonb +)) WITH ORDINALITY AS c ON TRUE +"""); + } + + public override async Task SelectMany_required_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_required_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT r."Id", c."Name", c."CollectionLeaf", c."OptionalReferenceLeaf", c."RequiredReferenceLeaf" +FROM "RootEntities" AS r +JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."RequiredReferenceTrunk" -> 'CollectionBranch') AS ( + "Name" text, + "CollectionLeaf" jsonb, + "OptionalReferenceLeaf" jsonb, + "RequiredReferenceLeaf" jsonb +)) WITH ORDINALITY AS c ON TRUE +"""); + } + + public override async Task SelectMany_optional_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_optional_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT r."Id", c."Name", c."CollectionLeaf", c."OptionalReferenceLeaf", c."RequiredReferenceLeaf" +FROM "RootEntities" AS r +JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."OptionalReferenceTrunk" -> 'CollectionBranch') AS ( + "Name" text, + "CollectionLeaf" jsonb, + "OptionalReferenceLeaf" jsonb, + "RequiredReferenceLeaf" jsonb +)) WITH ORDINALITY AS c ON TRUE +"""); + } + + public override async Task Project_branch_collection_element_using_indexer_constant(bool async) + { + await base.Project_branch_collection_element_using_indexer_constant(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'CollectionBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonProjectionGaussDBTest.cs new file mode 100644 index 0000000000..6b3c07c615 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonProjectionGaussDBTest.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedJsonProjectionSqlServerTest + : OwnedJsonProjectionRelationalTestBase +{ + public OwnedJsonProjectionSqlServerTest(OwnedJsonRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override Task Select_trunk_collection(bool async) + => AssertCantTrackJson(() => base.Select_trunk_collection(async)); + + public override Task Select_branch_required_collection(bool async) + => AssertCantTrackJson(() => base.Select_branch_required_collection(async)); + + public override Task Select_branch_optional_collection(bool async) + => AssertCantTrackJson(() => base.Select_branch_optional_collection(async)); + + public override Task Project_branch_collection_element_using_indexer_constant(bool async) + => AssertCantTrackJson(() => base.Project_branch_collection_element_using_indexer_constant(async)); + + public override Task Select_multiple_branch_leaf(bool async) + => AssertCantTrackJson(() => base.Select_multiple_branch_leaf(async)); + + public override Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + => AssertCantTrackJson(() => base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async)); + + public override Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + => AssertCantTrackJson(() => base.Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(async)); + + public override Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + => AssertCantTrackJson(() => base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async)); + + public override Task SelectMany_trunk_collection(bool async) + => AssertCantTrackJson(() => base.SelectMany_trunk_collection(async)); + + public override Task SelectMany_required_trunk_reference_branch_collection(bool async) + => AssertCantTrackJson(() => base.SelectMany_required_trunk_reference_branch_collection(async)); + + public override Task SelectMany_optional_trunk_reference_branch_collection(bool async) + => AssertCantTrackJson(() => base.SelectMany_optional_trunk_reference_branch_collection(async)); + + private async Task AssertCantTrackJson(Func test) + { + var message = (await Assert.ThrowsAsync(test)).Message; + + Assert.Equal(RelationalStrings.JsonEntityOrCollectionProjectedAtRootLevelInTrackingQuery("AsNoTracking"), message); + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonReferenceNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonReferenceNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..bf537a0028 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonReferenceNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedJsonReferenceNoTrackingProjectionGaussDBTest + : OwnedJsonReferenceNoTrackingProjectionRelationalTestBase +{ + public OwnedJsonReferenceNoTrackingProjectionGaussDBTest(OwnedJsonRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_root(bool async) + { + await base.Select_root(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_optional(bool async) + { + await base.Select_trunk_optional(async); + + AssertSql( + """ +SELECT r."OptionalReferenceTrunk", r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_required(bool async) + { + await base.Select_trunk_required(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk", r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'RequiredReferenceBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'OptionalReferenceBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'RequiredReferenceBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" -> 'OptionalReferenceBranch', r."Id" +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_root_duplicated(bool async) + { + await base.Select_root_duplicated(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_trunk_and_branch_duplicated(bool async) + { + await base.Select_trunk_and_branch_duplicated(async); + + AssertSql( + """ +SELECT r."OptionalReferenceTrunk", r."Id", r."OptionalReferenceTrunk" -> 'RequiredReferenceBranch', r."OptionalReferenceTrunk", r."OptionalReferenceTrunk" -> 'RequiredReferenceBranch' +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_trunk_and_trunk_duplicated(bool async) + { + await base.Select_trunk_and_trunk_duplicated(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk", r."Id", r."RequiredReferenceTrunk" #> '{OptionalReferenceBranch,RequiredReferenceLeaf}', r."RequiredReferenceTrunk", r."RequiredReferenceTrunk" #> '{OptionalReferenceBranch,RequiredReferenceLeaf}' +FROM "RootEntities" AS r +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_leaf_trunk_root(bool async) + { + await base.Select_leaf_trunk_root(async); + + AssertSql( + """ +SELECT r."RequiredReferenceTrunk" #> '{RequiredReferenceBranch,RequiredReferenceLeaf}', r."Id", r."RequiredReferenceTrunk", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT r1.c, r1."Id" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT r0."RequiredReferenceTrunk" -> 'RequiredReferenceBranch' AS c, r0."Id" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT r1.c, r1."Id" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT r0."OptionalReferenceTrunk" -> 'OptionalReferenceBranch' AS c, r0."Id" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r1 ON TRUE +ORDER BY r."Id" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonReferenceProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonReferenceProjectionGaussDBTest.cs new file mode 100644 index 0000000000..3302fd962d --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedJsonReferenceProjectionGaussDBTest.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedJsonReferenceProjectionSqlServerTest + : OwnedJsonReferenceProjectionRelationalTestBase +{ + public OwnedJsonReferenceProjectionSqlServerTest(OwnedJsonRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_root(bool async) + { + await base.Select_root(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk" +FROM "RootEntities" AS r +"""); + } + + public override async Task Select_root_duplicated(bool async) + { + await base.Select_root_duplicated(async); + + AssertSql( + """ +SELECT r."Id", r."Name", r."OptionalReferenceTrunkId", r."RequiredReferenceTrunkId", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk", r."CollectionTrunk", r."OptionalReferenceTrunk", r."RequiredReferenceTrunk" +FROM "RootEntities" AS r +"""); + } + + public override Task Select_trunk_optional(bool async) + => AssertCantTrackJson(() => base.Select_trunk_optional(async)); + + public override Task Select_trunk_required(bool async) + => AssertCantTrackJson(() => base.Select_trunk_required(async)); + + public override Task Select_branch_required_required(bool async) + => AssertCantTrackJson(() => base.Select_branch_required_required(async)); + + public override Task Select_branch_required_optional(bool async) + => AssertCantTrackJson(() => base.Select_branch_required_optional(async)); + + public override Task Select_branch_optional_required(bool async) + => AssertCantTrackJson(() => base.Select_branch_optional_required(async)); + + public override Task Select_branch_optional_optional(bool async) + => AssertCantTrackJson(() => base.Select_branch_optional_optional(async)); + + public override Task Select_trunk_and_branch_duplicated(bool async) + => AssertCantTrackJson(() => base.Select_trunk_and_branch_duplicated(async)); + + public override Task Select_trunk_and_trunk_duplicated(bool async) + => AssertCantTrackJson(() => base.Select_trunk_and_trunk_duplicated(async)); + + public override Task Select_leaf_trunk_root(bool async) + => AssertCantTrackJson(() => base.Select_leaf_trunk_root(async)); + + public override Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + => AssertCantTrackJson(() => base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async)); + + public override Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + => AssertCantTrackJson(() => base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async)); + + private async Task AssertCantTrackJson(Func test) + { + var message = (await Assert.ThrowsAsync(test)).Message; + + Assert.Equal(RelationalStrings.JsonEntityOrCollectionProjectedAtRootLevelInTrackingQuery("AsNoTracking"), message); + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..cb09f464b2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedNoTrackingProjectionGaussDBTest + : OwnedNoTrackingProjectionRelationalTestBase +{ + public OwnedNoTrackingProjectionGaussDBTest(OwnedRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_trunk_collection(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_trunk_collection(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_branch_required_collection(bool async) + { + await base.Select_branch_required_collection(async); + + AssertSql( + """ +SELECT r."Id", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +LEFT JOIN ( + SELECT r0."RelationshipsTrunkEntityRelationshipsRootEntityId", r0."Id1", r0."Name", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."RelationshipsBranchEntityId1", r1."Id1" AS "Id10", r1."Name" AS "Name0", r0."OptionalReferenceLeaf_Name", r0."RequiredReferenceLeaf_Name" + FROM "Root_RequiredReferenceTrunk_CollectionBranch" AS r0 + LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r1 ON r0."RelationshipsTrunkEntityRelationshipsRootEntityId" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r1."RelationshipsBranchEntityId1" +) AS s ON r."Id" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +ORDER BY r."Id" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_collection(bool async) + { + await base.Select_branch_optional_collection(async); + + AssertSql( + """ +SELECT r."Id", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +LEFT JOIN ( + SELECT r0."RelationshipsTrunkEntityRelationshipsRootEntityId", r0."Id1", r0."Name", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."RelationshipsBranchEntityId1", r1."Id1" AS "Id10", r1."Name" AS "Name0", r0."OptionalReferenceLeaf_Name", r0."RequiredReferenceLeaf_Name" + FROM "Root_RequiredReferenceTrunk_CollectionBranch" AS r0 + LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r1 ON r0."RelationshipsTrunkEntityRelationshipsRootEntityId" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r1."RelationshipsBranchEntityId1" +) AS s ON r."Id" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +ORDER BY r."Id" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + public override async Task Select_multiple_branch_leaf(bool async) + { + await base.Select_multiple_branch_leaf(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk_RequiredReferenceBranch_Name", r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r0."Id1", r0."Name", r."RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferen~", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."Id1", r1."Name", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r0 ON r."Id" = r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r1 ON r."Id" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN ( + SELECT r2."RelationshipsTrunkEntityRelationshipsRootEntityId", r2."Id1", r2."Name", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityId1", r3."Id1" AS "Id10", r3."Name" AS "Name0", r2."OptionalReferenceLeaf_Name", r2."RequiredReferenceLeaf_Name" + FROM "Root_RequiredReferenceTrunk_CollectionBranch" AS r2 + LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r3 ON r2."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r2."Id1" = r3."RelationshipsBranchEntityId1" +) AS s ON r."Id" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r0."Id1" NULLS FIRST, r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r1."Id1" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + public override async Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + { + await base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async); + + AssertSql( + """ +SELECT r."Id", r3."Id", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name", r3.c +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT 1 AS c, r0."Id" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r3 ON TRUE +LEFT JOIN ( + SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Id1", r1."Name", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."RelationshipsBranchEntityId1", r2."Id1" AS "Id10", r2."Name" AS "Name0", r1."OptionalReferenceLeaf_Name", r1."RequiredReferenceLeaf_Name" + FROM "Root_RequiredReferenceTrunk_CollectionBranch" AS r1 + LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r2."RelationshipsBranchEntityId1" +) AS s ON r3."Id" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +ORDER BY r."Id" NULLS FIRST, r3."Id" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + // https://github.com/dotnet/efcore/pull/35942 + public override Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + => Task.CompletedTask; + + public override async Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + { + await base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async); + + AssertSql( + """ +SELECT r."Id", r3."Id", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name", r3.c +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT 1 AS c, r0."Id" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r3 ON TRUE +LEFT JOIN ( + SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Id1", r1."Name", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."RelationshipsBranchEntityId1", r2."Id1" AS "Id10", r2."Name" AS "Name0", r1."OptionalReferenceLeaf_Name", r1."RequiredReferenceLeaf_Name" + FROM "Root_RequiredReferenceTrunk_CollectionBranch" AS r1 + LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r2."RelationshipsBranchEntityId1" +) AS s ON r."Id" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +ORDER BY r."Id" NULLS FIRST, r3."Id" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + public override async Task SelectMany_trunk_collection(bool async) + { + await base.SelectMany_trunk_collection(async); + + AssertSql( + """ +SELECT r0."RelationshipsRootEntityId", r0."Id1", r0."Name", r."Id", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."RelationshipsTrunkEntityId1", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityRelationshipsTrunkEntityId1", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name", r0."OptionalReferenceBranch_Name", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityRelationshipsTrunkEntityId1", r3."Id1", r3."Name", r0."OptionalReferenceBranch_OptionalReferenceLeaf_Name", r0."OptionalReferenceBranch_RequiredReferenceLeaf_Name", r0."RequiredReferenceBranch_Name", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityRelationshipsTrunkEntityId1", r4."Id1", r4."Name", r0."RequiredReferenceBranch_OptionalReferenceLeaf_Name", r0."RequiredReferenceBranch_RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "Root_CollectionTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +LEFT JOIN ( + SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."RelationshipsTrunkEntityId1", r1."Id1", r1."Name", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."RelationshipsBranchEntityRelationshipsTrunkEntityId1", r2."RelationshipsBranchEntityId1", r2."Id1" AS "Id10", r2."Name" AS "Name0", r1."OptionalReferenceLeaf_Name", r1."RequiredReferenceLeaf_Name" + FROM "Root_CollectionTrunk_CollectionBranch" AS r1 + LEFT JOIN "Root_CollectionTrunk_CollectionBranch_CollectionLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."RelationshipsTrunkEntityId1" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityId1" AND r1."Id1" = r2."RelationshipsBranchEntityId1" +) AS s ON r0."RelationshipsRootEntityId" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" AND r0."Id1" = s."RelationshipsTrunkEntityId1" +LEFT JOIN "Root_CollectionTrunk_OptionalReferenceBranch_CollectionLeaf" AS r3 ON CASE + WHEN r0."OptionalReferenceBranch_Name" IS NOT NULL THEN r0."RelationshipsRootEntityId" +END = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND CASE + WHEN r0."OptionalReferenceBranch_Name" IS NOT NULL THEN r0."Id1" +END = r3."RelationshipsBranchEntityRelationshipsTrunkEntityId1" +LEFT JOIN "Root_CollectionTrunk_RequiredReferenceBranch_CollectionLeaf" AS r4 ON r0."RelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityId1" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r0."Id1" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."RelationshipsTrunkEntityId1" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityId1" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST, s."Id10" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityId1" NULLS FIRST, r3."Id1" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityId1" NULLS FIRST +"""); + } + + public override async Task SelectMany_required_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_required_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT r0."RelationshipsTrunkEntityRelationshipsRootEntityId", r0."Id1", r0."Name", r."Id", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."RelationshipsBranchEntityId1", r1."Id1", r1."Name", r0."OptionalReferenceLeaf_Name", r0."RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "Root_RequiredReferenceTrunk_CollectionBranch" AS r0 ON r."Id" = r0."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r1 ON r0."RelationshipsTrunkEntityRelationshipsRootEntityId" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r1."RelationshipsBranchEntityId1" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r0."Id1" NULLS FIRST, r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r1."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + public override async Task SelectMany_optional_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_optional_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT r0."RelationshipsTrunkEntityRelationshipsRootEntityId", r0."Id1", r0."Name", r."Id", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."RelationshipsBranchEntityId1", r1."Id1", r1."Name", r0."OptionalReferenceLeaf_Name", r0."RequiredReferenceLeaf_Name" +FROM "RootEntities" AS r +INNER JOIN "Root_OptionalReferenceTrunk_CollectionBranch" AS r0 ON CASE + WHEN r."OptionalReferenceTrunk_Name" IS NOT NULL THEN r."Id" +END = r0."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_OptionalReferenceTrunk_CollectionBranch_CollectionLeaf" AS r1 ON r0."RelationshipsTrunkEntityRelationshipsRootEntityId" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r1."RelationshipsBranchEntityId1" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r0."Id1" NULLS FIRST, r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r1."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedProjectionGaussDBTest.cs new file mode 100644 index 0000000000..4bb6139a04 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedProjectionGaussDBTest.cs @@ -0,0 +1,57 @@ +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedProjectionGaussDBTest + : OwnedProjectionRelationalTestBase +{ + public OwnedProjectionGaussDBTest(OwnedRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override Task Select_trunk_collection(bool async) + => AssertCantTrackOwned(() => base.Select_trunk_collection(async)); + + public override Task Select_branch_required_collection(bool async) + => AssertCantTrackOwned(() => base.Select_branch_required_collection(async)); + + public override Task Select_branch_optional_collection(bool async) + => AssertCantTrackOwned(() => base.Select_branch_optional_collection(async)); + + public override Task Select_multiple_branch_leaf(bool async) + => AssertCantTrackOwned(() => base.Select_multiple_branch_leaf(async)); + + public override Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + => AssertCantTrackOwned(() => base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async)); + + public override Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + => AssertCantTrackOwned(() => base.Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(async)); + + public override Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + => AssertCantTrackOwned(() => base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async)); + + public override Task SelectMany_trunk_collection(bool async) + => AssertCantTrackOwned(() => base.SelectMany_trunk_collection(async)); + + public override Task SelectMany_required_trunk_reference_branch_collection(bool async) + => AssertCantTrackOwned(() => base.SelectMany_required_trunk_reference_branch_collection(async)); + + public override Task SelectMany_optional_trunk_reference_branch_collection(bool async) + => AssertCantTrackOwned(() => base.SelectMany_optional_trunk_reference_branch_collection(async)); + + private async Task AssertCantTrackOwned(Func test) + { + var message = (await Assert.ThrowsAsync(test)).Message; + + Assert.Equal(CoreStrings.OwnedEntitiesCannotBeTrackedWithoutTheirOwner, message); + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedReferenceNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedReferenceNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..0d009828d2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedReferenceNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedReferenceNoTrackingProjectionGaussDBTest + : OwnedReferenceNoTrackingProjectionRelationalTestBase +{ + public OwnedReferenceNoTrackingProjectionGaussDBTest(OwnedRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_root(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_root(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_trunk_optional(bool async) + { + await base.Select_trunk_optional(async); + + AssertSql( + """ +SELECT r."Id", r."OptionalReferenceTrunk_Name", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name", r."OptionalReferenceTrunk_OptionalReferenceBranch_Name", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."Id1", r2."Name", r."OptionalReferenceTrunk_OptionalReferenceBranch_OptionalReferen~", r."OptionalReferenceTrunk_OptionalReferenceBranch_RequiredReferen~", r."OptionalReferenceTrunk_RequiredReferenceBranch_Name", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."Id1", r3."Name", r."OptionalReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r."OptionalReferenceTrunk_RequiredReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN ( + SELECT r0."RelationshipsTrunkEntityRelationshipsRootEntityId", r0."Id1", r0."Name", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."RelationshipsBranchEntityId1", r1."Id1" AS "Id10", r1."Name" AS "Name0", r0."OptionalReferenceLeaf_Name", r0."RequiredReferenceLeaf_Name" + FROM "Root_OptionalReferenceTrunk_CollectionBranch" AS r0 + LEFT JOIN "Root_OptionalReferenceTrunk_CollectionBranch_CollectionLeaf" AS r1 ON r0."RelationshipsTrunkEntityRelationshipsRootEntityId" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r1."RelationshipsBranchEntityId1" +) AS s ON CASE + WHEN r."OptionalReferenceTrunk_Name" IS NOT NULL THEN r."Id" +END = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_OptionalReferenceTrunk_OptionalReferenceBranch_CollectionLeaf" AS r2 ON CASE + WHEN r."OptionalReferenceTrunk_OptionalReferenceBranch_Name" IS NOT NULL THEN r."Id" +END = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_OptionalReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r3 ON CASE + WHEN r."OptionalReferenceTrunk_RequiredReferenceBranch_Name" IS NOT NULL THEN r."Id" +END = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST, s."Id10" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r2."Id1" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_trunk_required(bool async) + { + await base.Select_trunk_required(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk_Name", s."RelationshipsTrunkEntityRelationshipsRootEntityId", s."Id1", s."Name", s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", s."RelationshipsBranchEntityId1", s."Id10", s."Name0", s."OptionalReferenceLeaf_Name", s."RequiredReferenceLeaf_Name", r."RequiredReferenceTrunk_OptionalReferenceBranch_Name", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."Id1", r2."Name", r."RequiredReferenceTrunk_OptionalReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_OptionalReferenceBranch_RequiredReferen~", r."RequiredReferenceTrunk_RequiredReferenceBranch_Name", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."Id1", r3."Name", r."RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN ( + SELECT r0."RelationshipsTrunkEntityRelationshipsRootEntityId", r0."Id1", r0."Name", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."RelationshipsBranchEntityId1", r1."Id1" AS "Id10", r1."Name" AS "Name0", r0."OptionalReferenceLeaf_Name", r0."RequiredReferenceLeaf_Name" + FROM "Root_RequiredReferenceTrunk_CollectionBranch" AS r0 + LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r1 ON r0."RelationshipsTrunkEntityRelationshipsRootEntityId" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r0."Id1" = r1."RelationshipsBranchEntityId1" +) AS s ON r."Id" = s."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_CollectionLeaf" AS r2 ON CASE + WHEN r."RequiredReferenceTrunk_OptionalReferenceBranch_Name" IS NOT NULL THEN r."Id" +END = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r3 ON r."Id" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, s."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, s."Id1" NULLS FIRST, s."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, s."RelationshipsBranchEntityId1" NULLS FIRST, s."Id10" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r2."Id1" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk_RequiredReferenceBranch_Name", r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r0."Id1", r0."Name", r."RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r0 ON r."Id" = r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk_OptionalReferenceBranch_Name", r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r0."Id1", r0."Name", r."RequiredReferenceTrunk_OptionalReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_OptionalReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_CollectionLeaf" AS r0 ON CASE + WHEN r."RequiredReferenceTrunk_OptionalReferenceBranch_Name" IS NOT NULL THEN r."Id" +END = r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk_RequiredReferenceBranch_Name", r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r0."Id1", r0."Name", r."RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r0 ON r."Id" = r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql( + """ +SELECT r."Id", r."RequiredReferenceTrunk_OptionalReferenceBranch_Name", r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r0."Id1", r0."Name", r."RequiredReferenceTrunk_OptionalReferenceBranch_OptionalReferen~", r."RequiredReferenceTrunk_OptionalReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_CollectionLeaf" AS r0 ON CASE + WHEN r."RequiredReferenceTrunk_OptionalReferenceBranch_Name" IS NOT NULL THEN r."Id" +END = r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_root_duplicated(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_root(async)); + + Assert.Equal("42702", exception.SqlState); + } + + // https://github.com/dotnet/efcore/issues/26993 + public override Task Select_trunk_and_branch_duplicated(bool async) + => Task.CompletedTask; + + // https://github.com/dotnet/efcore/issues/26993 + public override Task Select_trunk_and_trunk_duplicated(bool async) + => Task.CompletedTask; + + public override async Task Select_leaf_trunk_root(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_leaf_trunk_root(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + await base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async); + + AssertSql( + """ +SELECT r2."Id", r2."RequiredReferenceTrunk_RequiredReferenceBranch_Name", r."Id", r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r1."Id1", r1."Name", r2."RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r2."RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferen~" +FROM "RootEntities" AS r +LEFT JOIN LATERAL ( + SELECT r0."Id", r0."RequiredReferenceTrunk_RequiredReferenceBranch_Name", r0."RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferen~", r0."RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferen~" + FROM "RootEntities" AS r0 + ORDER BY r0."Id" NULLS FIRST + LIMIT 1 +) AS r2 ON TRUE +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r1 ON r2."Id" = r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r2."Id" NULLS FIRST, r1."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + // https://github.com/dotnet/efcore/issues/26993 + public override Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + => Task.CompletedTask; + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedReferenceProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedReferenceProjectionGaussDBTest.cs new file mode 100644 index 0000000000..9d13ddf4fb --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedReferenceProjectionGaussDBTest.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedReferenceProjectionGaussDBTest + : OwnedReferenceProjectionRelationalTestBase +{ + public OwnedReferenceProjectionGaussDBTest(OwnedRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_root(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_root(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_root_duplicated(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_root_duplicated(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override Task Select_trunk_optional(bool async) + => AssertCantTrackOwned(() => base.Select_trunk_optional(async)); + + public override Task Select_trunk_required(bool async) + => AssertCantTrackOwned(() => base.Select_trunk_required(async)); + + public override Task Select_branch_required_required(bool async) + => AssertCantTrackOwned(() => base.Select_branch_required_required(async)); + + public override Task Select_branch_required_optional(bool async) + => AssertCantTrackOwned(() => base.Select_branch_required_optional(async)); + + public override Task Select_branch_optional_required(bool async) + => AssertCantTrackOwned(() => base.Select_branch_optional_required(async)); + + public override Task Select_branch_optional_optional(bool async) + => AssertCantTrackOwned(() => base.Select_branch_optional_optional(async)); + + public override Task Select_trunk_and_branch_duplicated(bool async) + => AssertCantTrackOwned(() => base.Select_trunk_and_branch_duplicated(async)); + + public override Task Select_trunk_and_trunk_duplicated(bool async) + => AssertCantTrackOwned(() => base.Select_trunk_and_trunk_duplicated(async)); + + public override Task Select_leaf_trunk_root(bool async) + => AssertCantTrackOwned(() => base.Select_leaf_trunk_root(async)); + + public override Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + => AssertCantTrackOwned(() => base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async)); + + public override Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + => AssertCantTrackOwned(() => base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async)); + + private async Task AssertCantTrackOwned(Func test) + { + var message = (await Assert.ThrowsAsync(test)).Message; + + Assert.Equal(CoreStrings.OwnedEntitiesCannotBeTrackedWithoutTheirOwner, message); + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedTableSplittingNoTrackingProjectionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedTableSplittingNoTrackingProjectionGaussDBTest.cs new file mode 100644 index 0000000000..b1723b868b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedTableSplittingNoTrackingProjectionGaussDBTest.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedTableSplittingNoTrackingProjectionGaussDBTest + : OwnedTableSplittingNoTrackingProjectionRelationalTestBase +{ + public OwnedTableSplittingNoTrackingProjectionGaussDBTest(OwnedTableSplittingRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_trunk_collection(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_trunk_collection(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_branch_required_collection(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_branch_required_collection(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_branch_optional_collection(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_branch_optional_collection(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_multiple_branch_leaf(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_multiple_branch_leaf(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_subquery_root_set_trunk_FirstOrDefault_collection(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_subquery_root_set_trunk_FirstOrDefault_collection(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_subquery_root_set_complex_projection_including_references_to_outer_FirstOrDefault(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_subquery_root_set_complex_projection_FirstOrDefault_project_reference_to_outer(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task SelectMany_trunk_collection(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.SelectMany_trunk_collection(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task SelectMany_required_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_required_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Id1", r1."Name", r."Id", r0."RelationshipsRootEntityId", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."RelationshipsBranchEntityId1", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityId1", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityId1", r4."Id1", r4."Name", r2."Name", r3."Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +INNER JOIN "Root_RequiredReferenceTrunk_CollectionBranch" AS r1 ON r0."RelationshipsRootEntityId" = r1."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_OptionalReferenceLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r2."RelationshipsBranchEntityId1" +LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_RequiredReferenceLeaf" AS r3 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r3."RelationshipsBranchEntityId1" +LEFT JOIN "Root_RequiredReferenceTrunk_CollectionBranch_CollectionLeaf" AS r4 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r4."RelationshipsBranchEntityId1" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r1."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r1."Id1" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r2."RelationshipsBranchEntityId1" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityId1" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + public override async Task SelectMany_optional_trunk_reference_branch_collection(bool async) + { + await base.SelectMany_optional_trunk_reference_branch_collection(async); + + AssertSql( + """ +SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Id1", r1."Name", r."Id", r0."RelationshipsRootEntityId", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r2."RelationshipsBranchEntityId1", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityId1", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityId1", r4."Id1", r4."Name", r2."Name", r3."Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_OptionalReferenceTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +INNER JOIN "Root_OptionalReferenceTrunk_CollectionBranch" AS r1 ON r0."RelationshipsRootEntityId" = r1."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_OptionalReferenceTrunk_CollectionBranch_OptionalReferenceLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r2."RelationshipsBranchEntityId1" +LEFT JOIN "Root_OptionalReferenceTrunk_CollectionBranch_RequiredReferenceLeaf" AS r3 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r3."RelationshipsBranchEntityId1" +LEFT JOIN "Root_OptionalReferenceTrunk_CollectionBranch_CollectionLeaf" AS r4 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" AND r1."Id1" = r4."RelationshipsBranchEntityId1" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r1."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r1."Id1" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r2."RelationshipsBranchEntityId1" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityId1" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityId1" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedTableSplittingReferenceProjectionNoTrackingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedTableSplittingReferenceProjectionNoTrackingGaussDBTest.cs new file mode 100644 index 0000000000..5753fb7baf --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Relationships/Projection/OwnedTableSplittingReferenceProjectionNoTrackingGaussDBTest.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Projection; + +public class OwnedTableSplittingReferenceProjectionNoTrackingGaussDBTest + : OwnedTableSplittingReferenceProjectionNoTrackingRelationalTestBase +{ + public OwnedTableSplittingReferenceProjectionNoTrackingGaussDBTest(OwnedTableSplittingRelationshipsGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Select_root(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_root(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_trunk_optional(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_trunk_optional(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_trunk_required(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_trunk_required(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_branch_required_required(bool async) + { + await base.Select_branch_required_required(async); + + AssertSql( + """ +SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Name", r."Id", r0."RelationshipsRootEntityId", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."Id1", r4."Name", r2."Name", r3."Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch" AS r1 ON r0."RelationshipsRootEntityId" = r1."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferenceLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferenceLeaf" AS r3 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r4 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r1."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_required_optional(bool async) + { + await base.Select_branch_required_optional(async); + + AssertSql( + """ +SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Name", r."Id", r0."RelationshipsRootEntityId", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."Id1", r4."Name", r2."Name", r3."Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch" AS r1 ON r0."RelationshipsRootEntityId" = r1."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_OptionalReferenceLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_RequiredReferenceLeaf" AS r3 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_CollectionLeaf" AS r4 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r1."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_required(bool async) + { + await base.Select_branch_optional_required(async); + + AssertSql( + """ +SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Name", r."Id", r0."RelationshipsRootEntityId", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."Id1", r4."Name", r2."Name", r3."Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch" AS r1 ON r0."RelationshipsRootEntityId" = r1."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_OptionalReferenceLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_RequiredReferenceLeaf" AS r3 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_RequiredReferenceBranch_CollectionLeaf" AS r4 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r1."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_branch_optional_optional(bool async) + { + await base.Select_branch_optional_optional(async); + + AssertSql( + """ +SELECT r1."RelationshipsTrunkEntityRelationshipsRootEntityId", r1."Name", r."Id", r0."RelationshipsRootEntityId", r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~", r4."Id1", r4."Name", r2."Name", r3."Name" +FROM "RootEntities" AS r +LEFT JOIN "Root_RequiredReferenceTrunk" AS r0 ON r."Id" = r0."RelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch" AS r1 ON r0."RelationshipsRootEntityId" = r1."RelationshipsTrunkEntityRelationshipsRootEntityId" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_OptionalReferenceLeaf" AS r2 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_RequiredReferenceLeaf" AS r3 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +LEFT JOIN "Root_RequiredReferenceTrunk_OptionalReferenceBranch_CollectionLeaf" AS r4 ON r1."RelationshipsTrunkEntityRelationshipsRootEntityId" = r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" +ORDER BY r."Id" NULLS FIRST, r0."RelationshipsRootEntityId" NULLS FIRST, r1."RelationshipsTrunkEntityRelationshipsRootEntityId" NULLS FIRST, r2."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r3."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST, r4."RelationshipsBranchEntityRelationshipsTrunkEntityRelationships~" NULLS FIRST +"""); + } + + public override async Task Select_root_duplicated(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_root_duplicated(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_trunk_and_branch_duplicated(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_trunk_and_branch_duplicated(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_trunk_and_trunk_duplicated(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_trunk_and_trunk_duplicated(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_leaf_trunk_root(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_leaf_trunk_root(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_subquery_root_set_required_trunk_FirstOrDefault_branch(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_subquery_root_set_required_trunk_FirstOrDefault_branch(async)); + + Assert.Equal("42702", exception.SqlState); + } + + public override async Task Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(bool async) + { + // https://github.com/dotnet/efcore/issues/26993 + var exception = await Assert.ThrowsAsync(() => base.Select_subquery_root_set_optional_trunk_FirstOrDefault_branch(async)); + + Assert.Equal("42702", exception.SqlState); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/SharedTypeQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/SharedTypeQueryGaussDBTest.cs new file mode 100644 index 0000000000..9cd4a92418 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/SharedTypeQueryGaussDBTest.cs @@ -0,0 +1,10 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class SharedTypeQueryGaussDBTest(NonSharedFixture fixture) : SharedTypeQueryRelationalTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + 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 +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBFixture.cs new file mode 100644 index 0000000000..7f309f1102 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBFixture.cs @@ -0,0 +1,22 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class SpatialQueryGaussDBFixture : SpatialQueryRelationalFixture +{ + // We instruct the test store to pass a connection string to UseGaussDB() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an GaussDBDataSource internally. + protected override ITestStoreFactory TestStoreFactory + => new GaussDBTestStoreFactory(useConnectionString: true); + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection).AddEntityFrameworkGaussDBNetTopologySuite(); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("postgis"); + } + + // TODO: #1232 + // protected override bool CanExecuteQueryString => true; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBGeographyTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBGeographyTest.cs new file mode 100644 index 0000000000..f4b0a14f21 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBGeographyTest.cs @@ -0,0 +1,483 @@ +using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; +using NetTopologySuite; +using NetTopologySuite.Geometries; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +// ReSharper disable once UnusedMember.Global +[RequiresPostgis] +public class SpatialQueryGaussDBGeographyTest + : SpatialQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public SpatialQueryGaussDBGeographyTest(SpatialQueryGaussDBGeographyFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override bool AssertDistances + => false; + + public static IEnumerable IsAsyncDataAndUseSpheroid = + [ + [false, false], [false, true], [true, false], [true, true] + ]; + + public override async Task Area(bool async) + { + await base.Area(async); + + AssertSql( + """ +SELECT p."Id", ST_Area(p."Polygon") AS "Area" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task AsBinary(bool async) + { + await base.AsBinary(async); + + AssertSql( + """ +SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" +FROM "PointEntity" AS p +"""); + } + + public override async Task AsText(bool async) + { + await base.AsText(async); + + AssertSql( + """ +SELECT p."Id", ST_AsText(p."Point") AS "Text" +FROM "PointEntity" AS p +"""); + } + + public override async Task Buffer(bool async) + { + await base.Buffer(async); + + AssertSql( + """ +SELECT p."Id", ST_Buffer(p."Polygon", 1.0) AS "Buffer" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Buffer_quadrantSegments(bool async) + { + await base.Buffer_quadrantSegments(async); + + AssertSql( + """ +SELECT p."Id", ST_Buffer(p."Polygon", 1.0, 8) AS "Buffer" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Centroid(bool async) + { + await base.Centroid(async); + + AssertSql( + """ +SELECT p."Id", ST_Centroid(p."Polygon") AS "Centroid" +FROM "PolygonEntity" AS p +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncDataAndUseSpheroid))] + public async Task Distance_with_spheroid(bool async, bool useSpheroid) + { + var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); + + await AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Id, Distance = (double?)EF.Functions.Distance(e.Point, point, useSpheroid) }), + ss => ss.Set() + .Select(e => new { e.Id, Distance = (e.Point == null ? (double?)null : e.Point.Distance(point)) }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.Distance is null, a.Distance is null); + }); + + AssertSql( + $""" +@point='POINT (0 1)' (DbType = Object) +@useSpheroid='{useSpheroid}' + +SELECT p."Id", ST_Distance(p."Point", @point, @useSpheroid) AS "Distance" +FROM "PointEntity" AS p +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DistanceKnn(bool async) + { + var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); + + await AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Id, Distance = (double?)EF.Functions.DistanceKnn(e.Point, point) }), + ss => ss.Set() + .Select(e => new { e.Id, Distance = (e.Point == null ? (double?)null : e.Point.Distance(point)) }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.Distance is null, a.Distance is null); + }); + + AssertSql( + """ +@point='POINT (0 1)' (DbType = Object) + +SELECT p."Id", p."Point" <-> @point AS "Distance" +FROM "PointEntity" AS p +"""); + } + + public override async Task GeometryType(bool async) + { + // PostGIS returns "POINT", NTS returns "Point" + await AssertQuery( + async, + es => es.Set().Select( + e => new { e.Id, GeometryType = e.Point == null ? null : e.Point.GeometryType.ToLower() }), + x => x.Id); + + AssertSql( + """ +SELECT p."Id", CASE + WHEN p."Point" IS NULL THEN NULL + ELSE lower(GeometryType(p."Point")) +END AS "GeometryType" +FROM "PointEntity" AS p +"""); + } + + public override async Task Intersection(bool async) + { + await base.Intersection(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_Intersection(p."Polygon", @polygon) AS "Intersection" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Intersects(bool async) + { + await base.Intersects(async); + + AssertSql( + """ +@lineString='LINESTRING (0.5 -0.5, 0.5 0.5)' (DbType = Object) + +SELECT l."Id", ST_Intersects(l."LineString", @lineString) AS "Intersects" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task IsWithinDistance(bool async) + { + await base.IsWithinDistance(async); + + AssertSql( + """ +@point='POINT (0 1)' (DbType = Object) + +SELECT p."Id", ST_DWithin(p."Point", @point, 1.0) AS "IsWithinDistance" +FROM "PointEntity" AS p +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncDataAndUseSpheroid))] + public async Task IsWithinDistance_with_spheroid(bool async, bool useSpheroid) + { + var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); + + await AssertQuery( + async, + ss => ss.Set().Select( + e => new { e.Id, IsWithinDistance = (bool?)EF.Functions.IsWithinDistance(e.Point, point, 1, useSpheroid) }), + ss => ss.Set().Select( + e => new { e.Id, IsWithinDistance = e.Point == null ? (bool?)null : e.Point.IsWithinDistance(point, 1) }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + + if (e.IsWithinDistance is null) + { + Assert.False(a.IsWithinDistance ?? false); + } + else + { + Assert.NotNull(a.IsWithinDistance); + } + }); + + AssertSql( + $""" +@point='POINT (0 1)' (DbType = Object) +@useSpheroid='{useSpheroid}' + +SELECT p."Id", ST_DWithin(p."Point", @point, 1.0, @useSpheroid) AS "IsWithinDistance" +FROM "PointEntity" AS p +"""); + } + + public override async Task Length(bool async) + { + await base.Length(async); + + AssertSql( + """ +SELECT l."Id", ST_Length(l."LineString") AS "Length" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task SRID(bool async) + { + await base.SRID(async); + + AssertSql( + """ +SELECT p."Id", ST_SRID(p."Point") AS "SRID" +FROM "PointEntity" AS p +"""); + } + + public override async Task ToBinary(bool async) + { + await base.ToBinary(async); + + AssertSql( + """ +SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" +FROM "PointEntity" AS p +"""); + } + + public override async Task ToText(bool async) + { + await base.ToText(async); + + AssertSql( + """ +SELECT p."Id", ST_AsText(p."Point") AS "Text" +FROM "PointEntity" AS p +"""); + } + + #region Not supported on geography + + public override Task Boundary(bool async) + => Task.CompletedTask; + + public override Task Combine_aggregate(bool async) + => Task.CompletedTask; + + public override Task EnvelopeCombine_aggregate(bool async) + => Task.CompletedTask; + + public override Task Contains(bool async) + => Task.CompletedTask; + + public override Task ConvexHull(bool async) + => Task.CompletedTask; + + public override Task ConvexHull_aggregate(bool async) + => Task.CompletedTask; + + public override Task Crosses(bool async) + => Task.CompletedTask; + + public override Task Difference(bool async) + => Task.CompletedTask; + + public override Task Dimension(bool async) + => Task.CompletedTask; + + public override Task Disjoint_with_cast_to_nullable(bool async) + => Task.CompletedTask; + + public override Task Disjoint_with_null_check(bool async) + => Task.CompletedTask; + + public override Task EndPoint(bool async) + => Task.CompletedTask; + + public override Task Envelope(bool async) + => Task.CompletedTask; + + public override Task EqualsTopologically(bool async) + => Task.CompletedTask; + + public override Task ExteriorRing(bool async) + => Task.CompletedTask; + + public override Task GetGeometryN(bool async) + => Task.CompletedTask; + + public override Task GetGeometryN_with_null_argument(bool async) + => Task.CompletedTask; + + public override Task GetInteriorRingN(bool async) + => Task.CompletedTask; + + public override Task GetPointN(bool async) + => Task.CompletedTask; + + public override Task ICurve_IsClosed(bool async) + => Task.CompletedTask; + + public override Task IGeometryCollection_Count(bool async) + => Task.CompletedTask; + + public override Task IMultiCurve_IsClosed(bool async) + => Task.CompletedTask; + + public override Task IsEmpty(bool async) + => Task.CompletedTask; + + public override Task IsEmpty_equal_to_null(bool async) + => Task.CompletedTask; + + public override Task IsEmpty_not_equal_to_null(bool async) + => Task.CompletedTask; + + public override Task IsRing(bool async) + => Task.CompletedTask; + + public override Task IsSimple(bool async) + => Task.CompletedTask; + + public override Task IsValid(bool async) + => Task.CompletedTask; + + public override Task Item(bool async) + => Task.CompletedTask; + + public override Task InteriorPoint(bool async) + => Task.CompletedTask; + + public override Task LineString_Count(bool async) + => Task.CompletedTask; + + public override Task M(bool async) + => Task.CompletedTask; + + public override Task Normalized(bool async) + => Task.CompletedTask; + + public override Task NumGeometries(bool async) + => Task.CompletedTask; + + public override Task NumInteriorRings(bool async) + => Task.CompletedTask; + + public override Task NumPoints(bool async) + => Task.CompletedTask; + + public override Task OgcGeometryType(bool async) + => Task.CompletedTask; + + public override Task Overlaps(bool async) + => Task.CompletedTask; + + public override Task PointOnSurface(bool async) + => Task.CompletedTask; + + public override Task Relate(bool async) + => Task.CompletedTask; + + public override Task Reverse(bool async) + => Task.CompletedTask; + + public override Task StartPoint(bool async) + => Task.CompletedTask; + + public override Task SymmetricDifference(bool async) + => Task.CompletedTask; + + public override Task Touches(bool async) + => Task.CompletedTask; + + public override Task Union(bool async) + => Task.CompletedTask; + + public override Task Union_aggregate(bool async) + => Task.CompletedTask; + + public override Task Union_void(bool async) + => Task.CompletedTask; + + public override Task Within(bool async) + => Task.CompletedTask; + + public override Task X(bool async) + => Task.CompletedTask; + + public override Task Y(bool async) + => Task.CompletedTask; + + public override Task XY_with_collection_join(bool async) + => Task.CompletedTask; + + public override Task Z(bool async) + => Task.CompletedTask; + + #endregion + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class SpatialQueryGaussDBGeographyFixture : SpatialQueryGaussDBFixture + { + protected override string StoreName + => "SpatialQueryGeographyTest"; + + private NtsGeometryServices _geometryServices; + private GeometryFactory _geometryFactory; + + public NtsGeometryServices GeometryServices + => LazyInitializer.EnsureInitialized( + ref _geometryServices, + () => new NtsGeometryServices( + NtsGeometryServices.Instance.DefaultCoordinateSequenceFactory, + NtsGeometryServices.Instance.DefaultPrecisionModel, + 4326)); + + public override GeometryFactory GeometryFactory + => LazyInitializer.EnsureInitialized( + ref _geometryFactory, + () => GeometryServices.CreateGeometryFactory()); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new GaussDBDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(null, null, Ordinates.None, true); + + return optionsBuilder; + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBGeometryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBGeometryTest.cs new file mode 100644 index 0000000000..de27d36b24 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/SpatialQueryGaussDBGeometryTest.cs @@ -0,0 +1,782 @@ +using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; +using NetTopologySuite.Geometries; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +[RequiresPostgis] +public class SpatialQueryGaussDBGeometryTest + : SpatialQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public SpatialQueryGaussDBGeometryTest(SpatialQueryGaussDBGeometryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Area(bool async) + { + await base.Area(async); + + AssertSql( + """ +SELECT p."Id", ST_Area(p."Polygon") AS "Area" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task AsBinary(bool async) + { + await base.AsBinary(async); + + AssertSql( + """ +SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" +FROM "PointEntity" AS p +"""); + } + + public override async Task AsText(bool async) + { + await base.AsText(async); + + AssertSql( + """ +SELECT p."Id", ST_AsText(p."Point") AS "Text" +FROM "PointEntity" AS p +"""); + } + + public override async Task Boundary(bool async) + { + await base.Boundary(async); + + AssertSql( + """ +SELECT p."Id", ST_Boundary(p."Polygon") AS "Boundary" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Buffer(bool async) + { + await base.Buffer(async); + + AssertSql( + """ +SELECT p."Id", ST_Buffer(p."Polygon", 1.0) AS "Buffer" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Buffer_quadrantSegments(bool async) + { + await base.Buffer_quadrantSegments(async); + + AssertSql( + """ +SELECT p."Id", ST_Buffer(p."Polygon", 1.0, 8) AS "Buffer" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Centroid(bool async) + { + await base.Centroid(async); + + AssertSql( + """ +SELECT p."Id", ST_Centroid(p."Polygon") AS "Centroid" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Combine_aggregate(bool async) + { + await base.Combine_aggregate(async); + + AssertSql( + """ +SELECT p."Group" AS "Id", ST_Collect(p."Point") AS "Combined" +FROM "PointEntity" AS p +WHERE p."Point" IS NOT NULL +GROUP BY p."Group" +"""); + } + + public override async Task EnvelopeCombine_aggregate(bool async) + { + await base.EnvelopeCombine_aggregate(async); + + AssertSql( + """ +SELECT p."Group" AS "Id", ST_Extent(p."Point")::geometry AS "Combined" +FROM "PointEntity" AS p +WHERE p."Point" IS NOT NULL +GROUP BY p."Group" +"""); + } + + public override async Task Contains(bool async) + { + await base.Contains(async); + + AssertSql( + """ +@point='POINT (0.25 0.25)' (DbType = Object) + +SELECT p."Id", ST_Contains(p."Polygon", @point) AS "Contains" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task ConvexHull(bool async) + { + await base.ConvexHull(async); + + AssertSql( + """ +SELECT p."Id", ST_ConvexHull(p."Polygon") AS "ConvexHull" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task ConvexHull_aggregate(bool async) + { + await base.ConvexHull_aggregate(async); + + AssertSql( + """ +SELECT p."Group" AS "Id", ST_ConvexHull(ST_Collect(p."Point")) AS "ConvexHull" +FROM "PointEntity" AS p +WHERE p."Point" IS NOT NULL +GROUP BY p."Group" +"""); + } + + public override async Task IGeometryCollection_Count(bool async) + { + await base.IGeometryCollection_Count(async); + + AssertSql( + """ +SELECT m."Id", ST_NumGeometries(m."MultiLineString") AS "Count" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task LineString_Count(bool async) + { + await base.LineString_Count(async); + + AssertSql( + """ +SELECT l."Id", ST_NumPoints(l."LineString") AS "Count" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task Crosses(bool async) + { + await base.Crosses(async); + + AssertSql( + """ +@lineString='LINESTRING (0.5 -0.5, 0.5 0.5)' (DbType = Object) + +SELECT l."Id", ST_Crosses(l."LineString", @lineString) AS "Crosses" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task Difference(bool async) + { + await base.Difference(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_Difference(p."Polygon", @polygon) AS "Difference" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Dimension(bool async) + { + await base.Dimension(async); + + AssertSql( + """ +SELECT p."Id", ST_Dimension(p."Point") AS "Dimension" +FROM "PointEntity" AS p +"""); + } + + // PostGIS refuses to operate on points of mixed SRIDs + public override Task Distance_constant_srid_4326(bool async) + => Task.CompletedTask; + + public override async Task EndPoint(bool async) + { + await base.EndPoint(async); + + AssertSql( + """ +SELECT l."Id", ST_EndPoint(l."LineString") AS "EndPoint" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task Envelope(bool async) + { + await base.Envelope(async); + + AssertSql( + """ +SELECT p."Id", ST_Envelope(p."Polygon") AS "Envelope" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task EqualsTopologically(bool async) + { + await base.EqualsTopologically(async); + + AssertSql( + """ +@point='POINT (0 0)' (DbType = Object) + +SELECT p."Id", ST_Equals(p."Point", @point) AS "EqualsTopologically" +FROM "PointEntity" AS p +"""); + } + + public override async Task ExteriorRing(bool async) + { + await base.ExteriorRing(async); + + AssertSql( + """ +SELECT p."Id", ST_ExteriorRing(p."Polygon") AS "ExteriorRing" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task GeometryType(bool async) + { + // PostGIS returns "POINT", NTS returns "Point" + await AssertQuery( + async, + es => es.Set().Select( + e => new { e.Id, GeometryType = e.Point == null ? null : e.Point.GeometryType.ToLower() }), + x => x.Id); + + AssertSql( + """ +SELECT p."Id", CASE + WHEN p."Point" IS NULL THEN NULL + ELSE lower(GeometryType(p."Point")) +END AS "GeometryType" +FROM "PointEntity" AS p +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Force2D(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Id, Force2D = EF.Functions.Force2D(e.PointZ) }), + ss => ss.Set().Select(e => new { e.Id, Force2D = e.PointZ == null ? null : new Point(e.PointZ.X, e.PointZ.Y) }), + x => x.Id); + + AssertSql( + """ +SELECT p."Id", ST_Force2D(p."PointZ") AS "Force2D" +FROM "PointEntity" AS p +"""); + } + + public override async Task GetGeometryN(bool async) + { + await base.GetGeometryN(async); + + AssertSql( + """ +SELECT m."Id", ST_GeometryN(m."MultiLineString", 1) AS "Geometry0" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task GetInteriorRingN(bool async) + { + await base.GetInteriorRingN(async); + + AssertSql( + """ +SELECT p."Id", CASE + WHEN ST_NumInteriorRings(p."Polygon") = 0 THEN NULL + ELSE ST_InteriorRingN(p."Polygon", 1) +END AS "InteriorRing0" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task GetPointN(bool async) + { + await base.GetPointN(async); + + AssertSql( + """ +SELECT l."Id", ST_PointN(l."LineString", 1) AS "Point0" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task InteriorPoint(bool async) + { + await base.InteriorPoint(async); + + AssertSql( + """ +SELECT p."Id", ST_PointOnSurface(p."Polygon") AS "InteriorPoint", p."Polygon" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Intersection(bool async) + { + await base.Intersection(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_Intersection(p."Polygon", @polygon) AS "Intersection" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Intersects(bool async) + { + await base.Intersects(async); + + AssertSql( + """ +@lineString='LINESTRING (0.5 -0.5, 0.5 0.5)' (DbType = Object) + +SELECT l."Id", ST_Intersects(l."LineString", @lineString) AS "Intersects" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task ICurve_IsClosed(bool async) + { + await base.ICurve_IsClosed(async); + + AssertSql( + """ +SELECT l."Id", ST_IsClosed(l."LineString") AS "IsClosed" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task IMultiCurve_IsClosed(bool async) + { + await base.IMultiCurve_IsClosed(async); + + AssertSql( + """ +SELECT m."Id", ST_IsClosed(m."MultiLineString") AS "IsClosed" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task IsEmpty(bool async) + { + await base.IsEmpty(async); + + AssertSql( + """ +SELECT m."Id", ST_IsEmpty(m."MultiLineString") AS "IsEmpty" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task IsRing(bool async) + { + await base.IsRing(async); + + AssertSql( + """ +SELECT l."Id", ST_IsRing(l."LineString") AS "IsRing" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task IsSimple(bool async) + { + await base.IsSimple(async); + + AssertSql( + """ +SELECT l."Id", ST_IsSimple(l."LineString") AS "IsSimple" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task IsValid(bool async) + { + await base.IsValid(async); + + AssertSql( + """ +SELECT p."Id", ST_IsValid(p."Point") AS "IsValid" +FROM "PointEntity" AS p +"""); + } + + public override async Task IsWithinDistance(bool async) + { + await base.IsWithinDistance(async); + + AssertSql( + """ +@point='POINT (0 1)' (DbType = Object) + +SELECT p."Id", ST_DWithin(p."Point", @point, 1.0) AS "IsWithinDistance" +FROM "PointEntity" AS p +"""); + } + + public override async Task Item(bool async) + { + await base.Item(async); + + AssertSql( + """ +SELECT m."Id", ST_GeometryN(m."MultiLineString", 1) AS "Item0" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task Length(bool async) + { + await base.Length(async); + + AssertSql( + """ +SELECT l."Id", ST_Length(l."LineString") AS "Length" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task M(bool async) + { + await base.M(async); + + AssertSql( + """ +SELECT p."Id", ST_M(p."Point") AS "M" +FROM "PointEntity" AS p +"""); + } + + public override async Task NumGeometries(bool async) + { + await base.NumGeometries(async); + + AssertSql( + """ +SELECT m."Id", ST_NumGeometries(m."MultiLineString") AS "NumGeometries" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task NumInteriorRings(bool async) + { + await base.NumInteriorRings(async); + + AssertSql( + """ +SELECT p."Id", ST_NumInteriorRings(p."Polygon") AS "NumInteriorRings" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task NumPoints(bool async) + { + await base.NumPoints(async); + + AssertSql( + """ +SELECT l."Id", ST_NumPoints(l."LineString") AS "NumPoints" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task OgcGeometryType(bool async) + { + await base.OgcGeometryType(async); + + AssertSql( + """ +SELECT p."Id", CASE ST_GeometryType(p."Point") + WHEN 'ST_CircularString' THEN 8 + WHEN 'ST_CompoundCurve' THEN 9 + WHEN 'ST_CurvePolygon' THEN 10 + WHEN 'ST_GeometryCollection' THEN 7 + WHEN 'ST_LineString' THEN 2 + WHEN 'ST_MultiCurve' THEN 11 + WHEN 'ST_MultiLineString' THEN 5 + WHEN 'ST_MultiPoint' THEN 4 + WHEN 'ST_MultiPolygon' THEN 6 + WHEN 'ST_MultiSurface' THEN 12 + WHEN 'ST_Point' THEN 1 + WHEN 'ST_Polygon' THEN 3 + WHEN 'ST_PolyhedralSurface' THEN 15 + WHEN 'ST_Tin' THEN 16 +END AS "OgcGeometryType" +FROM "PointEntity" AS p +"""); + } + + public override async Task Overlaps(bool async) + { + await base.Overlaps(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_Overlaps(p."Polygon", @polygon) AS "Overlaps" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task PointOnSurface(bool async) + { + await base.PointOnSurface(async); + + AssertSql( + """ +SELECT p."Id", ST_PointOnSurface(p."Polygon") AS "PointOnSurface", p."Polygon" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Relate(bool async) + { + await base.Relate(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_Relate(p."Polygon", @polygon, '212111212') AS "Relate" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task SRID(bool async) + { + await base.SRID(async); + + AssertSql( + """ +SELECT p."Id", ST_SRID(p."Point") AS "SRID" +FROM "PointEntity" AS p +"""); + } + + public override async Task StartPoint(bool async) + { + await base.StartPoint(async); + + AssertSql( + """ +SELECT l."Id", ST_StartPoint(l."LineString") AS "StartPoint" +FROM "LineStringEntity" AS l +"""); + } + + public override async Task SymmetricDifference(bool async) + { + await base.SymmetricDifference(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_SymDifference(p."Polygon", @polygon) AS "SymmetricDifference" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task ToBinary(bool async) + { + await base.ToBinary(async); + + AssertSql( + """ +SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" +FROM "PointEntity" AS p +"""); + } + + public override async Task ToText(bool async) + { + await base.ToText(async); + + AssertSql( + """ +SELECT p."Id", ST_AsText(p."Point") AS "Text" +FROM "PointEntity" AS p +"""); + } + + public override async Task Touches(bool async) + { + await base.Touches(async); + + AssertSql( + """ +@polygon='POLYGON ((0 1, 1 0, 1 1, 0 1))' (DbType = Object) + +SELECT p."Id", ST_Touches(p."Polygon", @polygon) AS "Touches" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Union(bool async) + { + await base.Union(async); + + AssertSql( + """ +@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) + +SELECT p."Id", ST_Union(p."Polygon", @polygon) AS "Union" +FROM "PolygonEntity" AS p +"""); + } + + public override async Task Union_aggregate(bool async) + { + await base.Union_aggregate(async); + + AssertSql( + """ +SELECT p."Group" AS "Id", ST_Union(p."Point") AS "Union" +FROM "PointEntity" AS p +WHERE p."Point" IS NOT NULL +GROUP BY p."Group" +"""); + } + + public override async Task Union_void(bool async) + { + await base.Union_void(async); + + AssertSql( + """ +SELECT m."Id", ST_UnaryUnion(m."MultiLineString") AS "Union" +FROM "MultiLineStringEntity" AS m +"""); + } + + public override async Task Within(bool async) + { + await base.Within(async); + + AssertSql( + """ +@polygon='POLYGON ((-1 -1, 2 -1, 2 2, -1 2, -1 -1))' (DbType = Object) + +SELECT p."Id", ST_Within(p."Point", @polygon) AS "Within" +FROM "PointEntity" AS p +"""); + } + + public override async Task X(bool async) + { + await base.X(async); + + AssertSql( + """ +SELECT p."Id", ST_X(p."Point") AS "X" +FROM "PointEntity" AS p +"""); + } + + public override async Task Y(bool async) + { + await base.Y(async); + + AssertSql( + """ +SELECT p."Id", ST_Y(p."Point") AS "Y" +FROM "PointEntity" AS p +"""); + } + + public override async Task Z(bool async) + { + await base.Z(async); + + AssertSql( + """ +SELECT p."Id", ST_Z(p."Point") AS "Z" +FROM "PointEntity" AS p +"""); + } + + [ConditionalTheory(Skip = "#2850")] + [MemberData(nameof(IsAsyncData))] + public virtual async Task MultiString_Any(bool async) + { + 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 + // collection. + await AssertQuery( + async, + ss => ss.Set().Where(e => e.MultiLineString.Any(ls => ls == lineString)), + ss => ss.Set().Where( + e => e.MultiLineString != null && e.MultiLineString.Any(ls => GeometryComparer.Instance.Equals(ls, lineString))), + elementSorter: e => e.Id); + + AssertSql( + """ +@__lineString_0='LINESTRING (1 0, 1 1)' (DbType = Object) + +SELECT m."Id", m."MultiLineString" +FROM "MultiLineStringEntity" AS m +WHERE @__lineString_0 IN ( + SELECT m0.geom + FROM ST_Dump(m."MultiLineString") AS m0 +) +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class SpatialQueryGaussDBGeometryFixture : SpatialQueryGaussDBFixture + { + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new GaussDBDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(); + + return optionsBuilder; + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/SqlExecutorGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/SqlExecutorGaussDBTest.cs new file mode 100644 index 0000000000..1ea09c956b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/SqlExecutorGaussDBTest.cs @@ -0,0 +1,25 @@ +using System.Data.Common; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class SqlExecutorGaussDBTest : SqlExecutorTestBase> +{ + public SqlExecutorGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new GaussDBParameter { ParameterName = name, Value = value }; + + protected override string TenMostExpensiveProductsSproc + => """SELECT * FROM "Ten Most Expensive Products"()"""; + + protected override string CustomerOrderHistorySproc + => """SELECT * FROM "CustOrderHist"(@CustomerID)"""; + + protected override string CustomerOrderHistoryWithGeneratedParameterSproc + => """SELECT * FROM "CustOrderHist"({0})"""; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/SqlQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/SqlQueryGaussDBTest.cs new file mode 100644 index 0000000000..412b06ae78 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/SqlQueryGaussDBTest.cs @@ -0,0 +1,689 @@ +using System.Data.Common; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class SqlQueryGaussDBTest : SqlQueryTestBase> +{ + public SqlQueryGaussDBTest(NorthwindQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQueryRaw_queryable_simple(bool async) + { + await base.SqlQueryRaw_queryable_simple(async); + + AssertSql( + """ +SELECT * FROM "Customers" WHERE "ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order(bool async) + { + await base.SqlQueryRaw_queryable_simple_columns_out_of_order(async); + + AssertSql( + """ +SELECT "Region", "PostalCode", "Phone", "Fax", "CustomerID", "Country", "ContactTitle", "ContactName", "CompanyName", "City", "Address" FROM "Customers" +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(bool async) + { + await base.SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(async); + + AssertSql( + """ +SELECT "Region", "PostalCode", "PostalCode" AS "Foo", "Phone", "Fax", "CustomerID", "Country", "ContactTitle", "ContactName", "CompanyName", "City", "Address" FROM "Customers" +"""); + } + + // 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); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_after_removing_whitespaces(bool async) + { + await base.SqlQueryRaw_queryable_composed_after_removing_whitespaces(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + + +""" + " " + """ + + + + SELECT + * FROM "Customers" +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_DbParameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_DbParameter(async); + + AssertSql( + """ +customer='CONSH' (Nullable = false) + +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" WHERE "CustomerID" = @customer +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(async); + + AssertSql( + """ +p0='CONSH' (Nullable = false) + +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" WHERE "CustomerID" = @p0 +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_compiled_with_parameter(bool async) + { + await base.SqlQueryRaw_queryable_composed_compiled_with_parameter(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" WHERE "CustomerID" = 'CONSH' +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + + public override async Task SqlQueryRaw_composed_contains(bool async) + { + await base.SqlQueryRaw_composed_contains(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" +) AS m +WHERE m."CustomerID" IN ( + SELECT m0."CustomerID" + FROM ( + SELECT * FROM "Orders" + ) AS m0 +) +"""); + } + + public override async Task SqlQueryRaw_queryable_multiple_composed(bool async) + { + await base.SqlQueryRaw_queryable_multiple_composed(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode", m0."CustomerID", m0."EmployeeID", m0."Freight", m0."OrderDate", m0."OrderID", m0."RequiredDate", m0."ShipAddress", m0."ShipCity", m0."ShipCountry", m0."ShipName", m0."ShipPostalCode", m0."ShipRegion", m0."ShipVia", m0."ShippedDate" +FROM ( + SELECT * FROM "Customers" +) AS m +CROSS JOIN ( + SELECT * FROM "Orders" +) AS m0 +WHERE m."CustomerID" = m0."CustomerID" +"""); + } + + public override Task SqlQueryRaw_queryable_multiple_composed_with_closure_parameters(bool async) + // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but GaussDB rejects it because timestamptz + => Task.CompletedTask; + + public override Task SqlQueryRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) + // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but GaussDB rejects it because timestamptz + => Task.CompletedTask; + + public override async Task SqlQueryRaw_queryable_multiple_line_query(bool async) + { + await base.SqlQueryRaw_queryable_multiple_line_query(async); + + AssertSql( + """ +SELECT * +FROM "Customers" +WHERE "City" = 'London' +"""); + } + + public override async Task SqlQueryRaw_queryable_composed_multiple_line_query(bool async) + { + await base.SqlQueryRaw_queryable_composed_multiple_line_query(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * + FROM "Customers" +) AS m +WHERE m."City" = 'London' +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters(async); + + AssertSql( + """ +p0='London' +p1='Sales Representative' + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_inline(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters_inline(async); + + AssertSql( + """ +p0='London' +p1='Sales Representative' + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQuery_queryable_with_parameters_interpolated(bool async) + { + await base.SqlQuery_queryable_with_parameters_interpolated(async); + + AssertSql( + """ +p0='London' +p1='Sales Representative' + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQuery_queryable_with_parameters_inline_interpolated(bool async) + { + await base.SqlQuery_queryable_with_parameters_inline_interpolated(async); + + AssertSql( + """ +p0='London' +p1='Sales Representative' + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override Task SqlQuery_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(bool async) + // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but GaussDB rejects it because timestamptz + => Task.CompletedTask; + + public override async Task SqlQueryRaw_queryable_with_null_parameter(bool async) + { + await base.SqlQueryRaw_queryable_with_null_parameter(async); + + AssertSql( + """ +p0=NULL (Nullable = false) (DbType = Object) + +SELECT * FROM "Employees" WHERE "ReportsTo" = @p0 OR ("ReportsTo" IS NULL AND @p0 IS NULL) +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_and_closure(bool async) + { + var queryString = await base.SqlQueryRaw_queryable_with_parameters_and_closure(async); + + AssertSql( + """ +p0='London' +@contactTitle='Sales Representative' + +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 +) AS m +WHERE m."ContactTitle" = @contactTitle +"""); + + return null; + } + + public override async Task SqlQueryRaw_queryable_simple_cache_key_includes_query_string(bool async) + { + await base.SqlQueryRaw_queryable_simple_cache_key_includes_query_string(async); + + AssertSql( + """ +SELECT * FROM "Customers" WHERE "City" = 'London' +""", + // + """ +SELECT * FROM "Customers" WHERE "City" = 'Seattle' +"""); + } + + public override async Task SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(bool async) + { + await base.SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(async); + + AssertSql( + """ +p0='London' +p1='Sales Representative' + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +""", + // + """ +p0='Madrid' +p1='Accounting Manager' + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(bool async) + { + await base.SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(async); + + AssertSql( + """ +SELECT * FROM "Customers" +"""); + } + + public override async Task SqlQueryRaw_queryable_simple_projection_composed(bool async) + { + await base.SqlQueryRaw_queryable_simple_projection_composed(async); + + AssertSql( + """ +SELECT m."ProductName" +FROM ( + SELECT * + FROM "Products" + WHERE "Discontinued" <> TRUE + AND (("UnitsInStock" + "UnitsOnOrder") < "ReorderLevel") +) AS m +"""); + } + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] + public override async Task SqlQueryRaw_annotations_do_not_affect_successive_calls(bool async) + { + await base.SqlQueryRaw_annotations_do_not_affect_successive_calls(async); + + AssertSql( + """ +SELECT * FROM "Customers" WHERE "ContactName" LIKE '%z%' +""", + // + """ +SELECT * FROM "Customers" +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter(bool async) + { + await base.SqlQueryRaw_with_dbParameter(async); + + AssertSql( + """ +@city='London' (Nullable = false) + +SELECT * FROM "Customers" WHERE "City" = @city +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_without_name_prefix(bool async) + { + await base.SqlQueryRaw_with_dbParameter_without_name_prefix(async); + AssertSql( + """ +city='London' (Nullable = false) + +SELECT * FROM "Customers" WHERE "City" = @city +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_mixed(bool async) + { + await base.SqlQueryRaw_with_dbParameter_mixed(async); + + AssertSql( + """ +p0='London' +@title='Sales Representative' (Nullable = false) + +SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @title +""", + // + """ +@city='London' (Nullable = false) +p0='Sales Representative' + +SELECT * FROM "Customers" WHERE "City" = @city AND "ContactTitle" = @p0 +"""); + } + + public override async Task SqlQueryRaw_with_db_parameters_called_multiple_times(bool async) + { + await base.SqlQueryRaw_with_db_parameters_called_multiple_times(async); + + AssertSql( + """ +@id='ALFKI' (Nullable = false) + +SELECT * FROM "Customers" WHERE "CustomerID" = @id +""", + // + """ +@id='ALFKI' (Nullable = false) + +SELECT * FROM "Customers" WHERE "CustomerID" = @id +"""); + } + + public override async Task SqlQuery_with_inlined_db_parameter(bool async) + { + await base.SqlQuery_with_inlined_db_parameter(async); + + AssertSql( + """ +@somename='ALFKI' (Nullable = false) + +SELECT * FROM "Customers" WHERE "CustomerID" = @somename +"""); + } + + public override async Task SqlQuery_with_inlined_db_parameter_without_name_prefix(bool async) + { + await base.SqlQuery_with_inlined_db_parameter_without_name_prefix(async); + + AssertSql( + """ +somename='ALFKI' (Nullable = false) + +SELECT * FROM "Customers" WHERE "CustomerID" = @somename +"""); + } + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] + public override async Task SqlQuery_parameterization_issue_12213(bool async) + { + await base.SqlQuery_parameterization_issue_12213(async); + + AssertSql( + """ +p0='10300' + +SELECT m."OrderID" +FROM ( + SELECT * FROM "Orders" WHERE "OrderID" >= @p0 +) AS m +""", + // + """ +@__max_1='10400' +p0='10300' + +SELECT m."OrderID" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."OrderID" <= @__max_1 AND EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Orders" WHERE "OrderID" >= @p0 + ) AS m0 + WHERE m0."OrderID" = m."OrderID") +""", + // + """ +@__max_1='10400' +p0='10300' + +SELECT m."OrderID" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."OrderID" <= @__max_1 AND EXISTS ( + SELECT 1 + FROM ( + SELECT * FROM "Orders" WHERE "OrderID" >= @p0 + ) AS m0 + WHERE m0."OrderID" = m."OrderID") +"""); + } + + public override async Task SqlQueryRaw_does_not_parameterize_interpolated_string(bool async) + { + await base.SqlQueryRaw_does_not_parameterize_interpolated_string(async); + + AssertSql( + """ +p0='10250' + +SELECT * FROM "Orders" WHERE "OrderID" < @p0 +"""); + } + + public override async Task SqlQueryRaw_with_set_operation(bool async) + { + await base.SqlQueryRaw_with_set_operation(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT * FROM "Customers" WHERE "City" = 'London' +) AS m +UNION ALL +SELECT m0."Address", m0."City", m0."CompanyName", m0."ContactName", m0."ContactTitle", m0."Country", m0."CustomerID", m0."Fax", m0."Phone", m0."Region", m0."PostalCode" +FROM ( + SELECT * FROM "Customers" WHERE "City" = 'Berlin' +) AS m0 +"""); + } + + public override async Task Line_endings_after_Select(bool async) + { + await base.Line_endings_after_Select(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + SELECT + * FROM "Customers" +) AS m +WHERE m."City" = 'Seattle' +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_dbParameter(bool async) + { + await base.SqlQueryRaw_in_subquery_with_dbParameter(async); + + AssertSql( + """ +@city='London' (Nullable = false) + +SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."CustomerID" IN ( + SELECT m0."CustomerID" + FROM ( + SELECT * FROM "Customers" WHERE "City" = @city + ) AS m0 +) +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(bool async) + { + await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(async); + + AssertSql( + """ +p0='London' (Nullable = false) + +SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."CustomerID" IN ( + SELECT m0."CustomerID" + FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 + ) AS m0 +) +"""); + } + + public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(bool async) + { + await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(async); + + AssertSql( + """ +@city='London' (Nullable = false) + +SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."CustomerID" IN ( + SELECT m0."CustomerID" + FROM ( + SELECT * FROM "Customers" WHERE "City" = @city + ) AS m0 +) +"""); + } + + public override async Task SqlQueryRaw_with_dbParameter_mixed_in_subquery(bool async) + { + await base.SqlQueryRaw_with_dbParameter_mixed_in_subquery(async); + + AssertSql( + """ +p0='London' +@title='Sales Representative' (Nullable = false) + +SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."CustomerID" IN ( + SELECT m0."CustomerID" + FROM ( + SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @title + ) AS m0 +) +""", + // + """ +@city='London' (Nullable = false) +p0='Sales Representative' + +SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" +FROM ( + SELECT * FROM "Orders" +) AS m +WHERE m."CustomerID" IN ( + SELECT m0."CustomerID" + FROM ( + SELECT * FROM "Customers" WHERE "City" = @city AND "ContactTitle" = @p0 + ) AS m0 +) +"""); + } + + public override async Task SqlQueryRaw_composed_with_common_table_expression(bool async) + { + await base.SqlQueryRaw_composed_with_common_table_expression(async); + + AssertSql( + """ +SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" +FROM ( + WITH "Customers2" AS ( + SELECT * FROM "Customers" + ) + SELECT * FROM "Customers2" +) AS m +WHERE m."ContactName" LIKE '%z%' +"""); + } + +#pragma warning disable xUnit1026 + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] + public override Task Bad_data_error_handling_invalid_cast(bool async) + => Task.CompletedTask; + + [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] + public override Task Bad_data_error_handling_invalid_cast_projection(bool async) + => Task.CompletedTask; +#pragma warning restore xUnit1026 + + protected override DbParameter CreateDbParameter(string name, object value) + => new GaussDBParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCFiltersInheritanceQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCFiltersInheritanceQueryGaussDBFixture.cs new file mode 100644 index 0000000000..9788a70e1b --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCFiltersInheritanceQueryGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCFiltersInheritanceQueryGaussDBFixture : TPCInheritanceQueryGaussDBFixture +{ + public override bool EnableFilters + => true; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCFiltersInheritanceQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCFiltersInheritanceQueryGaussDBTest.cs new file mode 100644 index 0000000000..0df24f509a --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCFiltersInheritanceQueryGaussDBTest.cs @@ -0,0 +1,4 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCFiltersInheritanceQueryGaussDBTest(TPCFiltersInheritanceQueryGaussDBFixture fixture) + : TPCFiltersInheritanceQueryTestBase(fixture); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCGearsOfWarQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCGearsOfWarQueryGaussDBFixture.cs new file mode 100644 index 0000000000..659d9241be --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCGearsOfWarQueryGaussDBFixture.cs @@ -0,0 +1,95 @@ +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCGearsOfWarQueryGaussDBFixture : TPCGearsOfWarQueryRelationalFixture +{ + static TPCGearsOfWarQueryGaussDBFixture() + { + // TODO: Switch to using GaussDBDataSource +#pragma warning disable CS0618 // Type or member is obsolete + GaussDBConnection.GlobalTypeMapper.EnableRecordsAsTuples(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("uuid-ossp"); + + modelBuilder.Entity().Property(c => c.IssueDate).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); + } + + private GearsOfWarData? _expectedData; + + public override ISetSource GetExpectedData() + { + if (_expectedData is null) + { + _expectedData = (GearsOfWarData)base.GetExpectedData(); + + // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. + // Also chop sub-microsecond precision which GaussDB does not support. + foreach (var mission in _expectedData.Missions) + { + mission.Timeline = new DateTimeOffset( + mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + mission.Duration = new TimeSpan( + mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); + } + } + + return _expectedData; + } + + 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 GaussDB does not support. + => SeedForGaussDBAsync(context); + + public static async Task SeedForGaussDBAsync(GearsOfWarContext context) + { + var squads = GearsOfWarData.CreateSquads(); + var missions = GearsOfWarData.CreateMissions(); + var squadMissions = GearsOfWarData.CreateSquadMissions(); + var cities = GearsOfWarData.CreateCities(); + var weapons = GearsOfWarData.CreateWeapons(); + var tags = GearsOfWarData.CreateTags(); + var gears = GearsOfWarData.CreateGears(); + var locustLeaders = GearsOfWarData.CreateLocustLeaders(); + var factions = GearsOfWarData.CreateFactions(); + var locustHighCommands = GearsOfWarData.CreateHighCommands(); + + foreach (var mission in missions) + { + mission.Timeline = new DateTimeOffset( + mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + mission.Duration = new TimeSpan( + mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); + } + + GearsOfWarData.WireUp( + squads, missions, squadMissions, cities, weapons, tags, gears, locustLeaders, factions, locustHighCommands); + + context.Squads.AddRange(squads); + context.Missions.AddRange(missions); + context.SquadMissions.AddRange(squadMissions); + context.Cities.AddRange(cities); + context.Weapons.AddRange(weapons); + context.Tags.AddRange(tags); + context.Gears.AddRange(gears); + context.LocustLeaders.AddRange(locustLeaders); + context.Factions.AddRange(factions); + context.LocustHighCommands.AddRange(locustHighCommands); + await context.SaveChangesAsync(); + + GearsOfWarData.WireUp2(locustLeaders, factions); + + await context.SaveChangesAsync(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCGearsOfWarQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCGearsOfWarQueryGaussDBTest.cs new file mode 100644 index 0000000000..811b6142a1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCGearsOfWarQueryGaussDBTest.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCGearsOfWarQueryGaussDBTest : TPCGearsOfWarQueryRelationalTestBase +{ + public TPCGearsOfWarQueryGaussDBTest(TPCGearsOfWarQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // Base implementation uses DateTimeOffset.Now, which we don't translate by design. Use DateTimeOffset.UtcNow instead. + public override async Task Select_datetimeoffset_comparison_in_projection(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set().Select(m => m.Timeline > DateTimeOffset.UtcNow)); + + AssertSql( + """ +SELECT m."Timeline" > now() +FROM "Missions" AS m +"""); + } + + public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) + { + var dto = new DateTimeOffset(599898024001234567, new TimeSpan(0)); + var start = dto.AddDays(-1); + var end = dto.AddDays(1); + var dates = new[] { dto }; + + await AssertQuery( + async, + ss => ss.Set().Where( + m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline)), + assertEmpty: true); // TODO: Check this out + + AssertSql( + """ +@start='1902-01-01T10:00:00.1234567+00:00' (DbType = DateTime) +@end='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) +@dates={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) + +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" +FROM "Missions" AS m +WHERE @start <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @end AND m."Timeline" = ANY (@dates) +"""); + } + + public override async Task DateTimeOffset_Date_returns_datetime(bool async) + { + var dateTimeOffset = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Timeline.Date.ToLocalTime() >= dateTimeOffset.Date)); + + AssertSql( + """ +@dateTimeOffset_Date='0002-03-01T00:00:00.0000000' + +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 +"""); + } + + // Not supported by design: we support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a + // non-UTC DateTimeOffset. + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => Assert.ThrowsAsync(() => base.DateTimeOffsetNow_minus_timespan(async)); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCInheritanceQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCInheritanceQueryGaussDBFixture.cs new file mode 100644 index 0000000000..325d2d0f4c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCInheritanceQueryGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCInheritanceQueryGaussDBFixture : TPCInheritanceQueryFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCInheritanceQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCInheritanceQueryGaussDBTest.cs new file mode 100644 index 0000000000..8b346a4008 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCInheritanceQueryGaussDBTest.cs @@ -0,0 +1,8 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCInheritanceQueryGaussDBTest(TPCInheritanceQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : TPCInheritanceQueryTestBase(fixture, testOutputHelper) +{ + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyNoTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyNoTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..e8e0cd822c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyNoTrackingQueryGaussDBTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCManyToManyNoTrackingQueryGaussDBTest : TPCManyToManyNoTrackingQueryRelationalTestBase +{ + public TPCManyToManyNoTrackingQueryGaussDBTest(TPCManyToManyQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyQueryGaussDBFixture.cs new file mode 100644 index 0000000000..6edae86962 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyQueryGaussDBFixture.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCManyToManyQueryGaussDBFixture : TPCManyToManyQueryRelationalFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyQueryGaussDBTest.cs new file mode 100644 index 0000000000..e82e6fac48 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCManyToManyQueryGaussDBTest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCManyToManyQueryGaussDBTest : TPCManyToManyQueryRelationalTestBase +{ + public TPCManyToManyQueryGaussDBTest(TPCManyToManyQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPCRelationshipsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPCRelationshipsQueryGaussDBTest.cs new file mode 100644 index 0000000000..79eb8ae031 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPCRelationshipsQueryGaussDBTest.cs @@ -0,0 +1,19 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPCRelationshipsQueryGaussDBTest + : TPCRelationshipsQueryTestBase +{ + public TPCRelationshipsQueryGaussDBTest( + TPCRelationshipsQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + } + + public class TPCRelationshipsQueryGaussDBFixture : TPCRelationshipsQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPHFiltersInheritanceQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPHFiltersInheritanceQueryGaussDBFixture.cs new file mode 100644 index 0000000000..65e2143d80 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPHFiltersInheritanceQueryGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPHFiltersInheritanceQueryGaussDBFixture : TPHInheritanceQueryGaussDBFixture +{ + public override bool EnableFilters + => true; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPHInheritanceQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPHInheritanceQueryGaussDBFixture.cs new file mode 100644 index 0000000000..38cb5c60f4 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPHInheritanceQueryGaussDBFixture.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPHInheritanceQueryGaussDBFixture : TPHInheritanceQueryFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().HasNoKey().ToSqlQuery(""" + SELECT * FROM "Animals" + """); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPHInheritanceQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPHInheritanceQueryGaussDBTest.cs new file mode 100644 index 0000000000..4e88c93525 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPHInheritanceQueryGaussDBTest.cs @@ -0,0 +1,4 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPHInheritanceQueryGaussDBTest(TPHInheritanceQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : TPHInheritanceQueryTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTFiltersInheritanceQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTFiltersInheritanceQueryGaussDBFixture.cs new file mode 100644 index 0000000000..a096b11906 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTFiltersInheritanceQueryGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTFiltersInheritanceQuerySqlServerFixture : TPTInheritanceQueryGaussDBFixture +{ + public override bool EnableFilters + => true; +} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTFiltersInheritanceQueryNpgsqlTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTFiltersInheritanceQueryGaussDBTest.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/Query/TPTFiltersInheritanceQueryNpgsqlTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/TPTFiltersInheritanceQueryGaussDBTest.cs diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTGearsOfWarQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTGearsOfWarQueryGaussDBFixture.cs new file mode 100644 index 0000000000..676151b5ef --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTGearsOfWarQueryGaussDBFixture.cs @@ -0,0 +1,95 @@ +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTGearsOfWarQueryGaussDBFixture : TPTGearsOfWarQueryRelationalFixture +{ + static TPTGearsOfWarQueryGaussDBFixture() + { + // TODO: Switch to using GaussDBDataSource +#pragma warning disable CS0618 // Type or member is obsolete + GaussDBConnection.GlobalTypeMapper.EnableRecordsAsTuples(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("uuid-ossp"); + + modelBuilder.Entity().Property(c => c.IssueDate).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); + } + + private GearsOfWarData? _expectedData; + + public override ISetSource GetExpectedData() + { + if (_expectedData is null) + { + _expectedData = (GearsOfWarData)base.GetExpectedData(); + + // GearsOfWarData contains DateTimeOffsets with various offsets, which we don't support. Change these to UTC. + // Also chop sub-microsecond precision which GaussDB does not support. + foreach (var mission in _expectedData.Missions) + { + mission.Timeline = new DateTimeOffset( + mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + mission.Duration = new TimeSpan( + mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); + } + } + + return _expectedData; + } + + 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 GaussDB does not support. + => SeedForGaussDBAsync(context); + + public static async Task SeedForGaussDBAsync(GearsOfWarContext context) + { + var squads = GearsOfWarData.CreateSquads(); + var missions = GearsOfWarData.CreateMissions(); + var squadMissions = GearsOfWarData.CreateSquadMissions(); + var cities = GearsOfWarData.CreateCities(); + var weapons = GearsOfWarData.CreateWeapons(); + var tags = GearsOfWarData.CreateTags(); + var gears = GearsOfWarData.CreateGears(); + var locustLeaders = GearsOfWarData.CreateLocustLeaders(); + var factions = GearsOfWarData.CreateFactions(); + var locustHighCommands = GearsOfWarData.CreateHighCommands(); + + foreach (var mission in missions) + { + mission.Timeline = new DateTimeOffset( + mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); + mission.Duration = new TimeSpan( + mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); + } + + GearsOfWarData.WireUp( + squads, missions, squadMissions, cities, weapons, tags, gears, locustLeaders, factions, locustHighCommands); + + context.Squads.AddRange(squads); + context.Missions.AddRange(missions); + context.SquadMissions.AddRange(squadMissions); + context.Cities.AddRange(cities); + context.Weapons.AddRange(weapons); + context.Tags.AddRange(tags); + context.Gears.AddRange(gears); + context.LocustLeaders.AddRange(locustLeaders); + context.Factions.AddRange(factions); + context.LocustHighCommands.AddRange(locustHighCommands); + await context.SaveChangesAsync(); + + GearsOfWarData.WireUp2(locustLeaders, factions); + + await context.SaveChangesAsync(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTGearsOfWarQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTGearsOfWarQueryGaussDBTest.cs new file mode 100644 index 0000000000..127eb23ca2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTGearsOfWarQueryGaussDBTest.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTGearsOfWarQueryGaussDBTest : TPTGearsOfWarQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public TPTGearsOfWarQueryGaussDBTest(TPTGearsOfWarQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // TODO: #1232 + // protected override bool CanExecuteQueryString => true; + + // Base implementation uses DateTimeOffset.Now, which we don't translate by design. Use DateTimeOffset.UtcNow instead. + public override async Task Select_datetimeoffset_comparison_in_projection(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set().Select(m => m.Timeline > DateTimeOffset.UtcNow)); + + AssertSql( + """ +SELECT m."Timeline" > now() +FROM "Missions" AS m +"""); + } + + public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) + { + var dto = new DateTimeOffset(599898024001234567, new TimeSpan(0)); + var start = dto.AddDays(-1); + var end = dto.AddDays(1); + var dates = new[] { dto }; + + await AssertQuery( + async, + ss => ss.Set().Where( + m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline)), + assertEmpty: true); // TODO: Check this out + + AssertSql( + """ +@start='1902-01-01T10:00:00.1234567+00:00' (DbType = DateTime) +@end='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) +@dates={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) + +SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" +FROM "Missions" AS m +WHERE @start <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @end AND m."Timeline" = ANY (@dates) +"""); + } + + public override async Task DateTimeOffset_Date_returns_datetime(bool async) + { + var dateTimeOffset = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(m => m.Timeline.Date.ToLocalTime() >= dateTimeOffset.Date)); + + AssertSql( + """ +@dateTimeOffset_Date='0002-03-01T00:00:00.0000000' + +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 +"""); + } + + // Not supported by design: we support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a + // non-UTC DateTimeOffset. + public override Task DateTimeOffsetNow_minus_timespan(bool async) + => Assert.ThrowsAsync(() => base.DateTimeOffsetNow_minus_timespan(async)); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTInheritanceQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTInheritanceQueryGaussDBFixture.cs new file mode 100644 index 0000000000..428f3cb855 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTInheritanceQueryGaussDBFixture.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTInheritanceQueryGaussDBFixture : TPTInheritanceQueryFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTInheritanceQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTInheritanceQueryGaussDBTest.cs new file mode 100644 index 0000000000..08fa4499cd --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTInheritanceQueryGaussDBTest.cs @@ -0,0 +1,4 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTInheritanceQueryGaussDBTest(TPTInheritanceQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : TPTInheritanceQueryTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyNoTrackingQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyNoTrackingQueryGaussDBTest.cs new file mode 100644 index 0000000000..755a82c1b4 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyNoTrackingQueryGaussDBTest.cs @@ -0,0 +1,15 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTManyToManyNoTrackingQueryGaussDBTest : TPTManyToManyNoTrackingQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public TPTManyToManyNoTrackingQueryGaussDBTest(TPTManyToManyQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // TODO: #1232 + // protected override bool CanExecuteQueryString => true; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyQueryGaussDBFixture.cs new file mode 100644 index 0000000000..38415edda5 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyQueryGaussDBFixture.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTManyToManyQueryGaussDBFixture : TPTManyToManyQueryRelationalFixture +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); + modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyQueryGaussDBTest.cs new file mode 100644 index 0000000000..399da6f6c7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTManyToManyQueryGaussDBTest.cs @@ -0,0 +1,15 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTManyToManyQueryGaussDBTest : TPTManyToManyQueryRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public TPTManyToManyQueryGaussDBTest(TPTManyToManyQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // TODO: #1232 + // protected override bool CanExecuteQueryString => true; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/TPTRelationshipsQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/TPTRelationshipsQueryGaussDBTest.cs new file mode 100644 index 0000000000..ee64f60c30 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/TPTRelationshipsQueryGaussDBTest.cs @@ -0,0 +1,20 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class TPTRelationshipsQueryGaussDBTest + : TPTRelationshipsQueryTestBase +{ + // ReSharper disable once UnusedParameter.Local + public TPTRelationshipsQueryGaussDBTest( + TPTRelationshipsQueryGaussDBFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + } + + public class TPTRelationshipsQueryGaussDBFixture : TPTRelationshipsQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/ToSqlQueryGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/ToSqlQueryGaussDBTest.cs new file mode 100644 index 0000000000..0e38acb3bf --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/ToSqlQueryGaussDBTest.cs @@ -0,0 +1,18 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class ToSqlQuerySqlServerTest(NonSharedFixture fixture) : ToSqlQueryTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + // Base test implementation does not properly use identifier delimiters in raw SQL and isn't usable on GaussDB + public override Task Entity_type_with_navigation_mapped_to_SqlQuery(bool async) + => Task.CompletedTask; + + private void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/BasicTypesQueryGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/BasicTypesQueryGaussDBFixture.cs new file mode 100644 index 0000000000..d9f44be73f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/BasicTypesQueryGaussDBFixture.cs @@ -0,0 +1,100 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class BasicTypesQueryGaussDBFixture : BasicTypesQueryFixtureBase, ITestSqlLoggerFactory +{ + private BasicTypesData? _expectedData; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)).SetPostgresVersion(TestEnvironment.PostgresVersion); + return builder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + => modelBuilder.HasPostgresExtension("uuid-ossp"); + + protected override Task SeedAsync(BasicTypesContext context) + { + _expectedData ??= LoadAndTweakData(); + context.AddRange(_expectedData.BasicTypesEntities); + context.AddRange(_expectedData.NullableBasicTypesEntities); + return context.SaveChangesAsync(); + } + + public override ISetSource GetExpectedData() + => _expectedData ??= LoadAndTweakData(); + + private BasicTypesData LoadAndTweakData() + { + var data = (BasicTypesData)base.GetExpectedData(); + + foreach (var item in data.BasicTypesEntities) + { + // For all relevant temporal types, chop sub-microsecond precision which GaussDB does not support. + // Temporal types which aren't set (default) get mapped to -infinity on GaussDB; this value causes many tests to fail. + + if (item.DateTime == default) + { + item.DateTime += TimeSpan.FromSeconds(1); + } + + // GaussDB maps DateTime to timestamptz by default, but that represents UTC timestamps which require DateTimeKind.Utc. + item.DateTime = DateTime.SpecifyKind(new DateTime(StripSubMicrosecond(item.DateTime.Ticks)), DateTimeKind.Utc); + + if (item.DateOnly == default) + { + item.DateOnly = item.DateOnly.AddDays(1); + } + + item.TimeOnly = new TimeOnly(StripSubMicrosecond(item.TimeOnly.Ticks)); + item.TimeSpan = new TimeSpan(StripSubMicrosecond(item.TimeSpan.Ticks)); + + if (item.DateTimeOffset == default) + { + item.DateTimeOffset += TimeSpan.FromSeconds(1); + } + + // GaussDB doesn't have a real DateTimeOffset type; we map .NET DateTimeOffset to timestamptz, which represents a UTC + // timestamp, and so we only support offset=0. + // Also chop sub-microsecond precision which GaussDB does not support. + item.DateTimeOffset = new DateTimeOffset(StripSubMicrosecond(item.DateTimeOffset.Ticks), TimeSpan.Zero); + } + + // Do the same for the nullable counterparts + foreach (var item in data.NullableBasicTypesEntities) + { + if (item.DateTime.HasValue) + { + item.DateTime = DateTime.SpecifyKind(new DateTime(StripSubMicrosecond(item.DateTime.Value.Ticks)), DateTimeKind.Utc); + } + + if (item.TimeOnly.HasValue) + { + item.TimeOnly = new TimeOnly(StripSubMicrosecond(item.TimeOnly.Value.Ticks)); + } + + if (item.TimeSpan.HasValue) + { + item.TimeSpan = new TimeSpan(StripSubMicrosecond(item.TimeSpan.Value.Ticks)); + } + + if (item.DateTimeOffset.HasValue) + { + item.DateTimeOffset = new DateTimeOffset(StripSubMicrosecond(item.DateTimeOffset.Value.Ticks), TimeSpan.Zero); + } + } + + return data; + + static long StripSubMicrosecond(long ticks) => ticks - (ticks % (TimeSpan.TicksPerMillisecond / 1000)); + } + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs index 23f3f8aaf0..a3aa3e27e3 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs @@ -152,7 +152,7 @@ protected override string StoreName => "BigIntegerQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/ByteArrayTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/ByteArrayTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..ebd6d48260 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/ByteArrayTranslationsGaussDBTest.cs @@ -0,0 +1,103 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class ByteArrayTranslationsGaussDBTest : ByteArrayTranslationsTestBase +{ + // ReSharper disable once UnusedParameter.Local + public ByteArrayTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Length(bool async) + { + await base.Length(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."ByteArray") = 4 +"""); + } + + public override async Task Index(bool async) + { + await base.Index(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."ByteArray") >= 3 AND get_byte(b."ByteArray", 2) = 190 +"""); + } + + public override async Task First(bool async) + { + await base.First(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."ByteArray") >= 1 AND get_byte(b."ByteArray", 0)::smallint = 222 +"""); + } + + public override async Task Contains_with_constant(bool async) + { + await base.Contains_with_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE position(BYTEA E'\\x01' IN b."ByteArray") > 0 +"""); + } + + public override async Task Contains_with_parameter(bool async) + { + await base.Contains_with_parameter(async); + + AssertSql( + """ +@someByte='1' (DbType = Int16) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE position(set_byte(BYTEA E'\\x00', 0, @someByte) IN b."ByteArray") > 0 +"""); + } + + public override async Task Contains_with_column(bool async) + { + await base.Contains_with_column(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE position(set_byte(BYTEA E'\\x00', 0, b."Byte") IN b."ByteArray") > 0 +"""); + } + + public override async Task SequenceEqual(bool async) + { + await base.SequenceEqual(async); + + AssertSql( + """ +@byteArrayParam='0xDEADBEEF' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."ByteArray" = @byteArrayParam +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/CitextTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/CitextTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/CitextTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/CitextTranslationsTest.cs index 06673f0109..862ffb03b6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/CitextTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/CitextTranslationsTest.cs @@ -3,7 +3,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations; /// -/// Tests operations on the PostgreSQL citext type. +/// Tests operations on the GaussDB citext type. /// public class CitextTranslationsTest : IClassFixture { @@ -320,7 +320,7 @@ protected override string StoreName => "CitextQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/EnumTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/EnumTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..db083ba3dc --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/EnumTranslationsGaussDBTest.cs @@ -0,0 +1,308 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class EnumTranslationsGaussDBTest : EnumTranslationsTestBase +{ + public EnumTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Equality + + public override async Task Equality_to_constant(bool async) + { + await base.Equality_to_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Enum" = 0 +"""); + } + + public override async Task Equality_to_parameter(bool async) + { + await base.Equality_to_parameter(async); + + AssertSql( + """ +@basicEnum='0' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Enum" = @basicEnum +"""); + } + + public override async Task Equality_nullable_enum_to_constant(bool async) + { + await base.Equality_nullable_enum_to_constant(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Enum" = 0 +"""); + } + + public override async Task Equality_nullable_enum_to_parameter(bool async) + { + await base.Equality_nullable_enum_to_parameter(async); + + AssertSql( + """ +@basicEnum='0' + +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Enum" = @basicEnum +"""); + } + + public override async Task Equality_nullable_enum_to_null_constant(bool async) + { + await base.Equality_nullable_enum_to_null_constant(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Enum" IS NULL +"""); + } + + public override async Task Equality_nullable_enum_to_null_parameter(bool async) + { + await base.Equality_nullable_enum_to_null_parameter(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Enum" IS NULL +"""); + } + + public override async Task Equality_nullable_enum_to_nullable_parameter(bool async) + { + await base.Equality_nullable_enum_to_nullable_parameter(async); + + AssertSql( + """ +@basicEnum='0' (Nullable = true) + +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."Enum" = @basicEnum +"""); + } + + #endregion Equality + + public override async Task Bitwise_and_enum_constant(bool async) + { + await base.Bitwise_and_enum_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 1 > 0 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 1 = 1 +"""); + } + + public override async Task Bitwise_and_integral_constant(bool async) + { + await base.Bitwise_and_integral_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 8 = 8 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum"::bigint & 8 = 8 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum"::smallint & 8 = 8 +"""); + } + + public override async Task Bitwise_and_nullable_enum_with_constant(bool async) + { + await base.Bitwise_and_nullable_enum_with_constant(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."FlagsEnum" & 8 > 0 +"""); + } + + public override async Task Where_bitwise_and_nullable_enum_with_null_constant(bool async) + { + await base.Where_bitwise_and_nullable_enum_with_null_constant(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."FlagsEnum" & NULL > 0 +"""); + } + + public override async Task Where_bitwise_and_nullable_enum_with_non_nullable_parameter(bool async) + { + await base.Where_bitwise_and_nullable_enum_with_non_nullable_parameter(async); + + AssertSql( + """ +@flagsEnum='8' + +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."FlagsEnum" & @flagsEnum > 0 +"""); + } + + public override async Task Where_bitwise_and_nullable_enum_with_nullable_parameter(bool async) + { + await base.Where_bitwise_and_nullable_enum_with_nullable_parameter(async); + + AssertSql( + """ +@flagsEnum='8' (Nullable = true) + +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."FlagsEnum" & @flagsEnum > 0 +""", + // + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."FlagsEnum" & NULL > 0 +"""); + } + + public override async Task Bitwise_or(bool async) + { + await base.Bitwise_or(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" | 8 > 0 +"""); + } + + public override async Task Bitwise_projects_values_in_select(bool async) + { + await base.Bitwise_projects_values_in_select(async); + + AssertSql( + """ +SELECT b."FlagsEnum" & 8 = 8 AS "BitwiseTrue", b."FlagsEnum" & 8 = 4 AS "BitwiseFalse", b."FlagsEnum" & 8 AS "BitwiseValue" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 8 = 8 +LIMIT 1 +"""); + } + + public override async Task HasFlag(bool async) + { + await base.HasFlag(async); + +AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 8 = 8 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 12 = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 8 = 8 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 8 = 8 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE 8 & b."FlagsEnum" = b."FlagsEnum" +""", + // + """ +SELECT b."FlagsEnum" & 8 = 8 AS "hasFlagTrue", b."FlagsEnum" & 4 = 4 AS "hasFlagFalse" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & 8 = 8 +LIMIT 1 +"""); + } + + public override async Task HasFlag_with_non_nullable_parameter(bool async) + { + await base.HasFlag_with_non_nullable_parameter(async); + + AssertSql( + """ +@flagsEnum='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & @flagsEnum = @flagsEnum +"""); + } + + public override async Task HasFlag_with_nullable_parameter(bool async) + { + await base.HasFlag_with_nullable_parameter(async); + + AssertSql( + """ +@flagsEnum='8' (Nullable = true) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."FlagsEnum" & @flagsEnum = @flagsEnum +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/EnumTranslationsTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/EnumTranslationsTest.cs index dc14325937..a3a238831b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/EnumTranslationsTest.cs @@ -1,4 +1,4 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; namespace Microsoft.EntityFrameworkCore.Query.Translations; @@ -315,10 +315,10 @@ public class EnumFixture : SharedStoreFixtureBase, IQueryFixtureBas protected override string StoreName => "EnumQueryTest"; - // We instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow - // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. + // We instruct the test store to pass a connection string to UseGaussDB() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an GaussDBDataSource internally. protected override ITestStoreFactory TestStoreFactory - => new NpgsqlTestStoreFactory(useConnectionString: true); + => new GaussDBTestStoreFactory(useConnectionString: true); public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; @@ -327,7 +327,7 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build { var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder) + new GaussDBDbContextOptionsBuilder(optionsBuilder) .MapEnum("mapped_enum", "test") .MapEnum("inferred_enum", "test") .MapEnum("byte_enum", "test") diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/GuidTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/GuidTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..cd96453bda --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/GuidTranslationsGaussDBTest.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class GuidTranslationsGaussDBTest : GuidTranslationsTestBase +{ + // ReSharper disable once UnusedParameter.Local + public GuidTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task New_with_constant(bool async) + { + await base.New_with_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Guid" = 'df36f493-463f-4123-83f9-6b135deeb7ba' +"""); + } + + public override async Task New_with_parameter(bool async) + { + await base.New_with_parameter(async); + + AssertSql( + """ +@p='df36f493-463f-4123-83f9-6b135deeb7ba' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Guid" = @p +"""); + } + + public override async Task ToString_projection(bool async) + { + await base.ToString_projection(async); + + AssertSql( + """ +SELECT b."Guid"::text +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task NewGuid(bool async) + { + await base.NewGuid(async); + + if (TestEnvironment.PostgresVersion >= new Version(13, 0)) + { + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE gen_random_uuid() <> '00000000-0000-0000-0000-000000000000' +"""); + } + else + { + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE uuid_generate_v4() <> '00000000-0000-0000-0000-000000000000' +"""); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task CreateVersion7(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(od => Guid.CreateVersion7() != default)); + + if (TestEnvironment.PostgresVersion >= new Version(18, 0)) + { + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE uuidv7() <> '00000000-0000-0000-0000-000000000000' +"""); + } + else + { + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +"""); + } + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs similarity index 95% rename from test/EFCore.PG.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs index 34c5b4141b..6ab765cb14 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs @@ -176,8 +176,7 @@ public void LTree_matches_any_LQuery() AssertSql( """ -@lqueries={ '*.Astrophysics' -'*.Geology' } (DbType = Object) +@lqueries={ '*.Astrophysics', '*.Geology' } (DbType = Object) SELECT l."Id", l."LTree", l."LTreeAsString", l."LTrees", l."SomeString" FROM "LTreeEntities" AS l @@ -227,8 +226,7 @@ public void LTree_contains_any_LTree() Assert.Equal(4, count); AssertSql( """ -@ltrees={ 'Top.Science' -'Top.Art' } (DbType = Object) +@ltrees={ 'Top.Science', 'Top.Art' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -246,8 +244,7 @@ public void LTree_contained_by_any_LTree() Assert.Equal(3, count); AssertSql( """ -@ltrees={ 'Top.Science.Astronomy' -'Top.Art' } (DbType = Object) +@ltrees={ 'Top.Science.Astronomy', 'Top.Art' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -265,8 +262,7 @@ public void Any_LTree_matches_LQuery() AssertSql( """ -@ltrees={ 'Top.Science.Astronomy.Astrophysics' -'Top.Science.Astronomy.Cosmology' } (DbType = Object) +@ltrees={ 'Top.Science.Astronomy.Astrophysics', 'Top.Science.Astronomy.Cosmology' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -285,8 +281,7 @@ public void Any_LTree_matches_any_LQuery() AssertSql( """ -@lqueries={ '*.Astrophysics' -'*.Geology' } (DbType = Object) +@lqueries={ '*.Astrophysics', '*.Geology' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -320,8 +315,7 @@ public void First_LTree_ancestor() Assert.Equal(4, count); AssertSql( """ -@ltrees={ 'Top.Science' -'Top.Hobbies' } (DbType = Object) +@ltrees={ 'Top.Science', 'Top.Hobbies' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -340,8 +334,7 @@ public void First_LTree_descendant() Assert.Equal(3, count); AssertSql( """ -@ltrees={ 'Top.Science.Astronomy' -'Top.Hobbies.Amateurs_Astronomy' } (DbType = Object) +@ltrees={ 'Top.Science.Astronomy', 'Top.Hobbies.Amateurs_Astronomy' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -360,8 +353,7 @@ public void First_LTree_matches_LQuery() AssertSql( """ -@ltrees={ 'Top.Science.Astronomy.Astrophysics' -'Top.Science.Astronomy.Cosmology' } (DbType = Object) +@ltrees={ 'Top.Science.Astronomy.Astrophysics', 'Top.Science.Astronomy.Cosmology' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -379,8 +371,7 @@ public void First_LTree_matches_LTxtQuery() AssertSql( """ -@ltrees={ 'Top.Science.Astronomy.Astrophysics' -'Top.Science.Astronomy.Cosmology' } (DbType = Object) +@ltrees={ 'Top.Science.Astronomy.Astrophysics', 'Top.Science.Astronomy.Cosmology' } (DbType = Object) SELECT count(*)::int FROM "LTreeEntities" AS l @@ -563,7 +554,7 @@ protected override string StoreName => "LTreeQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MathTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MathTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..09b270d856 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MathTranslationsGaussDBTest.cs @@ -0,0 +1,761 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class MathTranslationsGaussDBTest : MathTranslationsTestBase +{ + public MathTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Abs_decimal(bool async) + { + await base.Abs_decimal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE abs(b."Decimal") = 9.5 +"""); + } + + public override async Task Abs_int(bool async) + { + await base.Abs_int(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE abs(b."Int") = 9 +"""); + } + + public override async Task Abs_double(bool async) + { + await base.Abs_double(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE abs(b."Double") = 9.5 +"""); + } + + public override async Task Abs_float(bool async) + { + await base.Abs_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE abs(b."Float")::double precision = 9.5 +"""); + } + + public override async Task Ceiling(bool async) + { + await base.Ceiling(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE ceiling(b."Double") = 9.0 +"""); + } + + public override async Task Ceiling_float(bool async) + { + await base.Ceiling_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE ceiling(b."Float") = 9 +"""); + } + + public override async Task Floor_decimal(bool async) + { + await base.Floor_decimal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(b."Decimal") = 8.0 +"""); + } + + public override async Task Floor_double(bool async) + { + await base.Floor_double(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(b."Double") = 8.0 +"""); + } + + public override async Task Floor_float(bool async) + { + await base.Floor_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(b."Float") = 8 +"""); + } + + public override async Task Power(bool async) + { + await base.Power(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE power(b."Int"::double precision, 2.0) = 64.0 +"""); + } + + public override async Task Power_float(bool async) + { + await base.Power_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE power(b."Float", 2) > 73 AND power(b."Float", 2) < 74 +"""); + } + + public override async Task Round_decimal(bool async) + { + await base.Round_decimal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE round(b."Decimal") = 9.0 +""", + // + """ +SELECT round(b."Decimal") +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Round_double(bool async) + { + await base.Round_double(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE round(b."Double") = 9.0 +""", + // + """ +SELECT round(b."Double") +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Round_float(bool async) + { + await base.Round_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE round(b."Float")::real = 9 +""", + // + """ +SELECT round(b."Float")::real +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Round_with_digits_decimal(bool async) + { + await base.Round_with_digits_decimal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE round(b."Decimal", 1) = 255.1 +"""); + } + + // GaussDB only has round(v, s) over numeric, may be possible to cast back and forth though + public override Task Round_with_digits_double(bool async) + => AssertTranslationFailed(() => base.Round_with_digits_double(async)); + + // GaussDB only has round(v, s) over numeric, may be possible to cast back and forth though + public override Task Round_with_digits_float(bool async) + => AssertTranslationFailed(() => base.Round_with_digits_float(async)); + + public override async Task Truncate_decimal(bool async) + { + await base.Truncate_decimal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE trunc(b."Decimal") = 8.0 +""", + // + """ +SELECT trunc(b."Decimal") +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Truncate_double(bool async) + { + await base.Truncate_double(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE trunc(b."Double") = 8.0 +""", + // + """ +SELECT trunc(b."Double") +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Truncate_float(bool async) + { + await base.Truncate_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE trunc(b."Float")::real = 8 +""", + // + """ +SELECT trunc(b."Float")::real +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Truncate_project_and_order_by_it_twice(bool async) + { + await base.Truncate_project_and_order_by_it_twice(async); + + AssertSql( + """ +SELECT trunc(b."Double") AS "A" +FROM "BasicTypesEntities" AS b +ORDER BY trunc(b."Double") NULLS FIRST +"""); + } + + // issue #16038 + // AssertSql( + // @"SELECT ROUND(CAST([o].[OrderID] AS float), 0, 1) AS [A] + //FROM [Orders] AS [o] + //WHERE [o].[OrderID] < 10250 + //ORDER BY [A]"); + public override async Task Truncate_project_and_order_by_it_twice2(bool async) + { + await base.Truncate_project_and_order_by_it_twice2(async); + + AssertSql( + """ +SELECT trunc(b."Double") AS "A" +FROM "BasicTypesEntities" AS b +ORDER BY trunc(b."Double") DESC NULLS LAST +"""); + } + + // issue #16038 + // AssertSql( + // @"SELECT ROUND(CAST([o].[OrderID] AS float), 0, 1) AS [A] + //FROM [Orders] AS [o] + //WHERE [o].[OrderID] < 10250 + //ORDER BY [A] DESC"); + public override async Task Truncate_project_and_order_by_it_twice3(bool async) + { + await base.Truncate_project_and_order_by_it_twice3(async); + + AssertSql( + """ +SELECT trunc(b."Double") AS "A" +FROM "BasicTypesEntities" AS b +ORDER BY trunc(b."Double") DESC NULLS LAST +"""); + } + + public override async Task Exp(bool async) + { + await base.Exp(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE exp(b."Double") > 1.0 +"""); + } + + public override async Task Exp_float(bool async) + { + await base.Exp_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE exp(b."Float") > 1 +"""); + } + + public override async Task Log(bool async) + { + await base.Log(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Double" > 0.0 AND ln(b."Double") <> 0.0 +"""); + } + + public override async Task Log_float(bool async) + { + await base.Log_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Float" > 0 AND ln(b."Float") <> 0 +"""); + } + + // GaussDB only has log(x, base) over numeric, may be possible to cast back and forth though + public override Task Log_with_newBase(bool async) + => AssertTranslationFailed(() => base.Log_with_newBase(async)); + + // GaussDB only has log(x, base) over numeric, may be possible to cast back and forth though + public override Task Log_with_newBase_float(bool async) + => AssertTranslationFailed(() => base.Log_with_newBase_float(async)); + + public override async Task Log10(bool async) + { + await base.Log10(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Double" > 0.0 AND log(b."Double") <> 0.0 +"""); + } + + public override async Task Log10_float(bool async) + { + await base.Log10_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Float" > 0 AND log(b."Float") <> 0 +"""); + } + + public override async Task Log2(bool async) + => await AssertTranslationFailed(() => base.Log2(async)); + + public override async Task Sqrt(bool async) + { + await base.Sqrt(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Double" > 0.0 AND sqrt(b."Double") > 0.0 +"""); + } + + public override async Task Sqrt_float(bool async) + { + await base.Sqrt_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Float" > 0 AND sqrt(b."Float") > 0 +"""); + } + + public override async Task Sign(bool async) + { + await base.Sign(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE sign(b."Double")::int > 0 +"""); + } + + public override async Task Sign_float(bool async) + { + await base.Sign_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE sign(b."Float")::int > 0 +"""); + } + + public override async Task Max(bool async) + { + await base.Max(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE GREATEST(b."Int", b."Short" - 3) = b."Int" +"""); + } + + public override async Task Max_nested(bool async) + { + await base.Max_nested(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE GREATEST(b."Short" - 3, b."Int", 1) = b."Int" +"""); + } + + public override async Task Max_nested_twice(bool async) + { + await base.Max_nested_twice(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE GREATEST(1, b."Int", 2, b."Short" - 3) = b."Int" +"""); + } + + public override async Task Min(bool async) + { + await base.Min(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE LEAST(b."Int", b."Short" + 3) = b."Int" +"""); + } + + public override async Task Min_nested(bool async) + { + await base.Min_nested(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE LEAST(b."Short" + 3, b."Int", 99999) = b."Int" +"""); + } + + public override async Task Min_nested_twice(bool async) + { + await base.Min_nested_twice(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE LEAST(99999, b."Int", 99998, b."Short" + 3) = b."Int" +"""); + } + + public override async Task Degrees(bool async) + { + await base.Degrees(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE degrees(b."Double") > 0.0 +"""); + } + + public override async Task Degrees_float(bool async) + { + await base.Degrees_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE degrees(b."Float") > 0 +"""); + } + + public override async Task Radians(bool async) + { + await base.Radians(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE radians(b."Double") > 0.0 +"""); + } + + public override async Task Radians_float(bool async) + { + await base.Radians_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE radians(b."Float") > 0 +"""); + } + + #region Trigonometry + + public override async Task Acos(bool async) + { + await base.Acos(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Double" >= -1.0 AND b."Double" <= 1.0 AND acos(b."Double") > 1.0 +"""); + } + + public override async Task Acos_float(bool async) + { + await base.Acos_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Float" >= -1 AND b."Float" <= 1 AND acos(b."Float") > 0 +"""); + } + + public override async Task Acosh(bool async) + => await AssertTranslationFailed(() => base.Acosh(async)); + + public override async Task Asin(bool async) + { + await base.Asin(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Double" >= -1.0 AND b."Double" <= 1.0 AND asin(b."Double") > -1.7976931348623157E+308 +"""); + } + + public override async Task Asin_float(bool async) + { + await base.Asin_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Float" >= -1 AND b."Float" <= 1 AND asin(b."Float")::double precision > -1.7976931348623157E+308 +"""); + } + + public override async Task Asinh(bool async) + => await AssertTranslationFailed(() => base.Asinh(async)); + + public override async Task Atan(bool async) + { + await base.Atan(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE atan(b."Double") > 0.0 +"""); + } + + public override async Task Atan_float(bool async) + { + await base.Atan_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE atan(b."Float") > 0 +"""); + } + + public override async Task Atanh(bool async) + => await AssertTranslationFailed(() => base.Atanh(async)); + + public override async Task Atan2(bool async) + { + await base.Atan2(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE atan2(b."Double", 1.0) > 0.0 +"""); + } + + public override async Task Atan2_float(bool async) + { + await base.Atan2_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE atan2(b."Float", 1) > 0 +"""); + } + + public override async Task Cos(bool async) + { + await base.Cos(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE cos(b."Double") > 0.0 +"""); + } + + public override async Task Cos_float(bool async) + { + await base.Cos_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE cos(b."Float") > 0 +"""); + } + + public override async Task Cosh(bool async) + => await AssertTranslationFailed(() => base.Cosh(async)); + + public override async Task Sin(bool async) + { + await base.Sin(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE sin(b."Double") > 0.0 +"""); + } + + public override async Task Sin_float(bool async) + { + await base.Sin_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE sin(b."Float") > 0 +"""); + } + + public override async Task Sinh(bool async) + => await AssertTranslationFailed(() => base.Sinh(async)); + + public override async Task Tan(bool async) + { + await base.Tan(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE tan(b."Double") > 0.0 +"""); + } + + public override async Task Tan_float(bool async) + { + await base.Tan_float(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE tan(b."Float") > 0 +"""); + } + + public override async Task Tanh(bool async) + => await AssertTranslationFailed(() => base.Tanh(async)); + + #endregion Trigonometry + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MiscellaneousTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MiscellaneousTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..58ac562854 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MiscellaneousTranslationsGaussDBTest.cs @@ -0,0 +1,411 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class MiscellaneousTranslationsGaussDBTest : MiscellaneousTranslationsRelationalTestBase +{ + public MiscellaneousTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Random + + public override async Task Random_on_EF_Functions(bool async) + { + await base.Random_on_EF_Functions(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "BasicTypesEntities" AS b +WHERE random() >= 0.0 AND random() < 1.0 +"""); + } + + public override async Task Random_Shared_Next_with_no_args(bool async) + { + await base.Random_Shared_Next_with_no_args(async); + + AssertSql(); + } + + public override async Task Random_Shared_Next_with_one_arg(bool async) + { + await base.Random_Shared_Next_with_one_arg(async); + + AssertSql(); + } + + public override async Task Random_Shared_Next_with_two_args(bool async) + { + await base.Random_Shared_Next_with_two_args(async); + + AssertSql(); + } + + public override async Task Random_new_Next_with_no_args(bool async) + { + await base.Random_new_Next_with_no_args(async); + + AssertSql(); + } + + public override async Task Random_new_Next_with_one_arg(bool async) + { + await base.Random_new_Next_with_one_arg(async); + + AssertSql(); + } + + public override async Task Random_new_Next_with_two_args(bool async) + { + await base.Random_new_Next_with_two_args(async); + + AssertSql(); + } + + #endregion Random + + #region Convert + + // These tests convert (among other things) to and from boolean, which GaussDB + // does not support (https://github.com/dotnet/efcore/issues/19606) + + public override async Task Convert_ToBoolean(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Convert_ToBoolean(async)); + Assert.Equal("42846", exception.SqlState); + } + + public override async Task Convert_ToByte(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Convert_ToByte(async)); + Assert.Equal("42846", exception.SqlState); + } + + public override async Task Convert_ToDecimal(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Convert_ToDecimal(async)); + Assert.Equal("42846", exception.SqlState); + } + + public override async Task Convert_ToDouble(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Convert_ToDouble(async)); + Assert.Equal("42846", exception.SqlState); + } + + public override async Task Convert_ToInt16(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Convert_ToInt16(async)); + Assert.Equal("42846", exception.SqlState); + } + + public override async Task Convert_ToInt32(bool async) + { + await base.Convert_ToInt32(async); + +AssertSql( +""" +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Bool"::int = 1 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Byte"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Decimal"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Double"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Float"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Short"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Long"::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text::int = 12 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::int = 12 +"""); + } + + public override async Task Convert_ToInt64(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Convert_ToInt64(async)); + Assert.Equal("42846", exception.SqlState); + } + + // Convert on DateTime not yet supported + public override Task Convert_ToString(bool async) + => AssertTranslationFailed(() => base.Convert_ToString(async)); + + #endregion Convert + + #region Compare + + public override async Task Int_Compare_to_simple_zero(bool async) + { + await base.Int_Compare_to_simple_zero(async); + +AssertSql( + """ +@orderId='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = @orderId +""", + // + """ +@orderId='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" <> @orderId +""", + // + """ +@orderId='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" > @orderId +""", + // + """ +@orderId='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" <= @orderId +""", + // + """ +@orderId='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" > @orderId +""", + // + """ +@orderId='8' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" <= @orderId +"""); + } + + public override async Task DateTime_Compare_to_simple_zero(bool async, bool compareTo) + { + // The base test implementation uses an Unspecified DateTime, which isn't supported with GaussDB timestamptz + var dateTime = new DateTime(1998, 5, 4, 15, 30, 10, DateTimeKind.Utc); + + if (compareTo) + { + await AssertQuery( + async, + ss => ss.Set().Where(c => c.DateTime.CompareTo(dateTime) == 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => 0 != c.DateTime.CompareTo(dateTime))); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.DateTime.CompareTo(dateTime) > 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => 0 >= c.DateTime.CompareTo(dateTime))); + + await AssertQuery( + async, + ss => ss.Set().Where(c => 0 < c.DateTime.CompareTo(dateTime))); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.DateTime.CompareTo(dateTime) <= 0)); + } + else + { + await AssertQuery( + async, + ss => ss.Set().Where(c => DateTime.Compare(c.DateTime, dateTime) == 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => 0 != DateTime.Compare(c.DateTime, dateTime))); + + await AssertQuery( + async, + ss => ss.Set().Where(c => DateTime.Compare(c.DateTime, dateTime) > 0)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => 0 >= DateTime.Compare(c.DateTime, dateTime))); + + await AssertQuery( + async, + ss => ss.Set().Where(c => 0 < DateTime.Compare(c.DateTime, dateTime))); + + await AssertQuery( + async, + ss => ss.Set().Where(c => DateTime.Compare(c.DateTime, dateTime) <= 0)); + } + +AssertSql( +""" +@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" = @dateTime +""", + // + """ +@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" <> @dateTime +""", + // + """ +@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" > @dateTime +""", + // + """ +@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" <= @dateTime +""", + // + """ +@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" > @dateTime +""", + // + """ +@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" <= @dateTime +"""); + } + + public override async Task TimeSpan_Compare_to_simple_zero(bool async, bool compareTo) + { + await base.TimeSpan_Compare_to_simple_zero(async, compareTo); + + AssertSql( + """ +@timeSpan='01:02:03' (DbType = Object) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan" = @timeSpan +""", + // + """ +@timeSpan='01:02:03' (DbType = Object) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan" <> @timeSpan +""", + // + """ +@timeSpan='01:02:03' (DbType = Object) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan" > @timeSpan +""", + // + """ +@timeSpan='01:02:03' (DbType = Object) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan" <= @timeSpan +""", + // + """ +@timeSpan='01:02:03' (DbType = Object) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan" > @timeSpan +""", + // + """ +@timeSpan='01:02:03' (DbType = Object) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan" <= @timeSpan +"""); + } + + #endregion Compare + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs similarity index 86% rename from test/EFCore.PG.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs index 0527c2b537..ba50bdf77c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs @@ -2,13 +2,13 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations; -[MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 -public class MultirangeTranslationsTest : IClassFixture +[MinimumPostgresVersion(14, 0)] // Multiranges were introduced in GaussDB 14 +public class MultirangeTranslationsTest : IClassFixture { - private MultirangeQueryNpgsqlFixture Fixture { get; } + private MultirangeQueryGaussDBFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public MultirangeTranslationsTest(MultirangeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public MultirangeTranslationsTest(MultirangeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); @@ -44,7 +44,7 @@ public void Contains_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(1, 2) }; + var multirange = new GaussDBRange[] { new(1, 2) }; var id = context.TestEntities .Single(x => x.IntMultirange.Contains(multirange)) @@ -67,7 +67,7 @@ public void Contains_range() { using var context = CreateContext(); - var range = new NpgsqlRange(1, 2); + var range = new GaussDBRange(1, 2); var id = context.TestEntities .Single(x => x.IntMultirange.Contains(range)) @@ -90,7 +90,7 @@ public void ContainedBy_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(1, 2) }; + var multirange = new GaussDBRange[] { new(1, 2) }; var id = context.TestEntities .Single(x => multirange.ContainedBy(x.IntMultirange)) @@ -113,7 +113,7 @@ public void Equals_operator() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(0, 5), new(7, 10) }; + var multirange = new GaussDBRange[] { new(0, 5), new(7, 10) }; var id = context.TestEntities .Single(x => x.IntMultirange == multirange) @@ -122,8 +122,7 @@ public void Equals_operator() AssertSql( """ -@multirange={ '[0,5]' -'[7,10]' } (DbType = Object) +@multirange={ '[0,5]', '[7,10]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -137,7 +136,7 @@ public void Equals_method() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(0, 5), new(7, 10) }; + var multirange = new GaussDBRange[] { new(0, 5), new(7, 10) }; var id = context.TestEntities .Single(x => x.IntMultirange.Equals(multirange)) @@ -146,8 +145,7 @@ public void Equals_method() AssertSql( """ -@multirange={ '[0,5]' -'[7,10]' } (DbType = Object) +@multirange={ '[0,5]', '[7,10]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -161,7 +159,7 @@ public void Overlaps_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(-3, 0), new(100, 101) }; + var multirange = new GaussDBRange[] { new(-3, 0), new(100, 101) }; var id = context.TestEntities .Single(x => x.IntMultirange.Overlaps(multirange)) @@ -170,8 +168,7 @@ public void Overlaps_multirange() AssertSql( """ -@multirange={ '[-3,0]' -'[100,101]' } (DbType = Object) +@multirange={ '[-3,0]', '[100,101]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -185,7 +182,7 @@ public void Overlaps_range() { using var context = CreateContext(); - var range = new NpgsqlRange(-3, 0); + var range = new GaussDBRange(-3, 0); var id = context.TestEntities .Single(x => x.IntMultirange.Overlaps(range)) @@ -208,7 +205,7 @@ public void IsStrictlyLeftOf_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(11, 13), new(15, 16) }; + var multirange = new GaussDBRange[] { new(11, 13), new(15, 16) }; var id = context.TestEntities .Single(x => x.IntMultirange.IsStrictlyLeftOf(multirange)) @@ -217,8 +214,7 @@ public void IsStrictlyLeftOf_multirange() AssertSql( """ -@multirange={ '[11,13]' -'[15,16]' } (DbType = Object) +@multirange={ '[11,13]', '[15,16]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -232,7 +228,7 @@ public void IsStrictlyLeftOf_range() { using var context = CreateContext(); - var range = new NpgsqlRange(11, 13); + var range = new GaussDBRange(11, 13); var id = context.TestEntities .Single(x => x.IntMultirange.IsStrictlyLeftOf(range)) @@ -255,7 +251,7 @@ public void IsStrictlyRightOf_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(-10, -7), new(-5, 3) }; + var multirange = new GaussDBRange[] { new(-10, -7), new(-5, 3) }; var id = context.TestEntities .Single(x => x.IntMultirange.IsStrictlyRightOf(multirange)) @@ -264,8 +260,7 @@ public void IsStrictlyRightOf_multirange() AssertSql( """ -@multirange={ '[-10,-7]' -'[-5,3]' } (DbType = Object) +@multirange={ '[-10,-7]', '[-5,3]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -279,7 +274,7 @@ public void IsStrictlyRightOf_range() { using var context = CreateContext(); - var range = new NpgsqlRange(-5, 3); + var range = new GaussDBRange(-5, 3); var id = context.TestEntities .Single(x => x.IntMultirange.IsStrictlyRightOf(range)) @@ -302,7 +297,7 @@ public void DoesNotExtendLeftOf_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(2, 7), new(13, 18) }; + var multirange = new GaussDBRange[] { new(2, 7), new(13, 18) }; var id = context.TestEntities .Single(x => x.IntMultirange.DoesNotExtendLeftOf(multirange)) @@ -311,8 +306,7 @@ public void DoesNotExtendLeftOf_multirange() AssertSql( """ -@multirange={ '[2,7]' -'[13,18]' } (DbType = Object) +@multirange={ '[2,7]', '[13,18]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -326,7 +320,7 @@ public void DoesNotExtendLeftOf_range() { using var context = CreateContext(); - var range = new NpgsqlRange(2, 7); + var range = new GaussDBRange(2, 7); var id = context.TestEntities .Single(x => x.IntMultirange.DoesNotExtendLeftOf(range)) @@ -349,7 +343,7 @@ public void DoesNotExtendRightOf_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(-5, -3), new(13, 18) }; + var multirange = new GaussDBRange[] { new(-5, -3), new(13, 18) }; var id = context.TestEntities .Single(x => x.IntMultirange.DoesNotExtendRightOf(multirange)) @@ -358,8 +352,7 @@ public void DoesNotExtendRightOf_multirange() AssertSql( """ -@multirange={ '[-5,-3]' -'[13,18]' } (DbType = Object) +@multirange={ '[-5,-3]', '[13,18]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -373,7 +366,7 @@ public void DoesNotExtendRightOf_range() { using var context = CreateContext(); - var range = new NpgsqlRange(13, 18); + var range = new GaussDBRange(13, 18); var id = context.TestEntities .Single(x => x.IntMultirange.DoesNotExtendRightOf(range)) @@ -396,7 +389,7 @@ public void IsAdjacentTo_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(-5, -4), new(-2, -1) }; + var multirange = new GaussDBRange[] { new(-5, -4), new(-2, -1) }; var id = context.TestEntities .Single(x => x.IntMultirange.IsAdjacentTo(multirange)) @@ -405,8 +398,7 @@ public void IsAdjacentTo_multirange() AssertSql( """ -@multirange={ '[-5,-4]' -'[-2,-1]' } (DbType = Object) +@multirange={ '[-5,-4]', '[-2,-1]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -420,7 +412,7 @@ public void IsAdjacentTo_range() { using var context = CreateContext(); - var range = new NpgsqlRange(-2, -1); + var range = new GaussDBRange(-2, -1); var id = context.TestEntities .Single(x => x.IntMultirange.IsAdjacentTo(range)) @@ -443,10 +435,10 @@ public void Union_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(-5, -1) }; + var multirange = new GaussDBRange[] { new(-5, -1) }; var id = context.TestEntities - .Single(x => x.IntMultirange.Union(multirange) == new NpgsqlRange[] { new(-5, 5), new(7, 10) }) + .Single(x => x.IntMultirange.Union(multirange) == new GaussDBRange[] { new(-5, 5), new(7, 10) }) .Id; Assert.Equal(1, id); @@ -466,17 +458,16 @@ public void Intersect_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(-5, 1), new(9, 13) }; + var multirange = new GaussDBRange[] { new(-5, 1), new(9, 13) }; var id = context.TestEntities - .Single(x => x.IntMultirange.Intersect(multirange) == new NpgsqlRange[] { new(0, 1), new(9, 10) }) + .Single(x => x.IntMultirange.Intersect(multirange) == new GaussDBRange[] { new(0, 1), new(9, 10) }) .Id; Assert.Equal(1, id); AssertSql( """ -@multirange={ '[-5,1]' -'[9,13]' } (DbType = Object) +@multirange={ '[-5,1]', '[9,13]' } (DbType = Object) SELECT t."Id", t."DateOnlyDateMultirange", t."DateTimeDateMultirange", t."DecimalMultirange", t."IntMultirange", t."LongMultirange" FROM "TestEntities" AS t @@ -516,10 +507,10 @@ public void Except_multirange() { using var context = CreateContext(); - var multirange = new NpgsqlRange[] { new(2, 3) }; + var multirange = new GaussDBRange[] { new(2, 3) }; var id = context.TestEntities - .Single(x => x.IntMultirange.Except(multirange) == new NpgsqlRange[] { new(0, 1), new(4, 5), new(7, 10) }) + .Single(x => x.IntMultirange.Except(multirange) == new GaussDBRange[] { new(0, 1), new(4, 5), new(7, 10) }) .Id; Assert.Equal(1, id); @@ -546,7 +537,7 @@ public void Is_empty() using var context = CreateContext(); var id = context.TestEntities - .Single(x => x.IntMultirange.Intersect(new NpgsqlRange[] { new(18, 19) }).Any()) + .Single(x => x.IntMultirange.Intersect(new GaussDBRange[] { new(18, 19) }).Any()) .Id; Assert.Equal(2, id); @@ -565,7 +556,7 @@ public void Merge() using var context = CreateContext(); var id = context.TestEntities - .Single(x => x.IntMultirange.Merge() == new NpgsqlRange(0, 10)) + .Single(x => x.IntMultirange.Merge() == new GaussDBRange(0, 10)) .Id; Assert.Equal(1, id); @@ -687,7 +678,7 @@ public void Contains() using var context = CreateContext(); var id = context.TestEntities - .Single(x => x.IntMultirange.Contains(new NpgsqlRange(0, 5))) + .Single(x => x.IntMultirange.Contains(new GaussDBRange(0, 5))) .Id; Assert.Equal(1, id); @@ -706,7 +697,7 @@ public void Skip_Contains() using var context = CreateContext(); var id = context.TestEntities - .Single(x => x.IntMultirange.Skip(1).Contains(new NpgsqlRange(7, 10))) + .Single(x => x.IntMultirange.Skip(1).Contains(new GaussDBRange(7, 10))) .Id; Assert.Equal(1, id); @@ -727,13 +718,13 @@ LIMIT 2 #region Fixtures - public class MultirangeQueryNpgsqlFixture : SharedStoreFixtureBase + public class MultirangeQueryGaussDBFixture : SharedStoreFixtureBase { protected override string StoreName => "MultirangeQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; @@ -745,13 +736,13 @@ protected override Task SeedAsync(MultirangeContext context) public class MultirangeTestEntity { public int Id { get; set; } - public NpgsqlRange[] IntMultirange { get; set; } = null!; - public NpgsqlRange[] LongMultirange { get; set; } = null!; - public NpgsqlRange[] DecimalMultirange { get; set; } = null!; - public NpgsqlRange[] DateOnlyDateMultirange { get; set; } = null!; + public GaussDBRange[] IntMultirange { get; set; } = null!; + public GaussDBRange[] LongMultirange { get; set; } = null!; + public GaussDBRange[] DecimalMultirange { get; set; } = null!; + public GaussDBRange[] DateOnlyDateMultirange { get; set; } = null!; [Column(TypeName = "datemultirange")] - public NpgsqlRange[] DateTimeDateMultirange { get; set; } = null!; + public GaussDBRange[] DateTimeDateMultirange { get; set; } = null!; } private void AssertSql(params string[] expected) diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NetworkTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NetworkTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..c1c597ff19 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NetworkTranslationsGaussDBTest.cs @@ -0,0 +1,1574 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Net; +using System.Net.NetworkInformation; + +// ReSharper disable ConvertToConstant.Local + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +/// +/// Provides unit tests for network address operator and function translations. +/// +/// +/// See: https://www.postgresql.org/docs/current/static/functions-net.html +/// +public class NetworkTranslationsGaussDBTest : IClassFixture +{ + private NetworkAddressQueryGaussDBFixture Fixture { get; } + + // ReSharper disable once UnusedParameter.Local + public NetworkTranslationsGaussDBTest(NetworkAddressQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region BugTests + + [Fact] + public void Demonstrate_ValueTypeParametersAreDuplicated() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrEqual(x.Cidr, cidr)) + .Select(x => x.Cidr.Equals(cidr)) + .ToArray(); + + AssertSql( + """ +@cidr='0.0.0.0/0' (DbType = Object) +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Cidr" = @cidr +FROM "NetTestEntities" AS n +WHERE n."Cidr" >>= @p +"""); + } + + #endregion + + #region ParseTests + + [Fact] + public void IPAddress_inet_parse_column() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => x.Inet.Equals(IPAddress.Parse(x.TextInet))); + + Assert.Equal(9, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" = n."TextInet"::inet +"""); + } + + [Fact] + public void PhysicalAddress_macaddr_parse_column() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => x.Macaddr.Equals(PhysicalAddress.Parse(x.TextMacaddr))); + + Assert.Equal(9, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr" = n."TextMacaddr"::macaddr +"""); + } + + [Fact] + public void IPAddress_inet_parse_literal() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => x.Inet.Equals(IPAddress.Parse("192.168.1.2"))); + + Assert.Equal(1, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" = INET '192.168.1.2' +"""); + } + + [Fact] + public void PhysicalAddress_macaddr_parse_literal() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => x.Macaddr.Equals(PhysicalAddress.Parse("12-34-56-00-00-02"))); + + Assert.Equal(1, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr" = MACADDR '123456000002' +"""); + } + + [Fact] + public void IPAddress_inet_parse_parameter() + { + using var context = CreateContext(); + var inet = "192.168.1.2"; + var count = context.NetTestEntities.Count(x => x.Inet.Equals(IPAddress.Parse(inet))); + + Assert.Equal(1, count); + } + + [Fact] + public void PhysicalAddress_macaddr_parse_parameter() + { + using var context = CreateContext(); + var macaddr = "12-34-56-00-00-01"; + var count = context.NetTestEntities.Count(x => x.Macaddr.Equals(PhysicalAddress.Parse(macaddr))); + + Assert.Equal(1, count); + } + + #endregion + + #region RelationalOperatorTests + + [Fact] + public void LessThan_IPAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.LessThan(x.Inet, IPAddress.Parse("192.168.1.7"))); + + Assert.Equal(6, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" < INET '192.168.1.7' +"""); + } + + [Fact] + public void LessThan_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.LessThan(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" < @p +"""); + } + + [Fact] + public void LessThan_PhysicalAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.LessThan(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); + + Assert.Equal(6, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr" < MACADDR '123456000007' +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void LessThan_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.LessThan(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); + + Assert.Equal(6, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr8" < MACADDR8 '08002B0102030407' +"""); + } + + [Fact] + public void LessThanOrEqual_IPAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.LessThanOrEqual(x.Inet, IPAddress.Parse("192.168.1.7"))); + + Assert.Equal(7, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" <= INET '192.168.1.7' +"""); + } + + [Fact] + public void LessThanOrEqual_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.LessThanOrEqual(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" <= @p +"""); + } + + [Fact] + public void LessThanOrEqual_PhysicalAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.LessThanOrEqual(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); + + Assert.Equal(7, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr" <= MACADDR '123456000007' +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void LessThanOrEqual_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count( + x => EF.Functions.LessThanOrEqual(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); + + Assert.Equal(7, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr8" <= MACADDR8 '08002B0102030407' +"""); + } + + [Fact] + public void GreaterThanOrEqual_IPAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.GreaterThanOrEqual(x.Inet, IPAddress.Parse("192.168.1.7"))); + + Assert.Equal(3, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" >= INET '192.168.1.7' +"""); + } + + [Fact] + public void GreaterThanOrEqual_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.GreaterThanOrEqual(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" >= @p +"""); + } + + [Fact] + public void GreaterThanOrEqual_PhysicalAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count( + x => EF.Functions.GreaterThanOrEqual(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); + + Assert.Equal(3, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr" >= MACADDR '123456000007' +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void GreaterThanOrEqual_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count( + x => EF.Functions.GreaterThanOrEqual(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); + + Assert.Equal(3, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr8" >= MACADDR8 '08002B0102030407' +"""); + } + + [Fact] + public void GreaterThan_IPAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.GreaterThan(x.Inet, IPAddress.Parse("192.168.1.7"))); + + Assert.Equal(2, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" > INET '192.168.1.7' +"""); + } + + [Fact] + public void GreaterThan_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.GreaterThan(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" > @p +"""); + } + + [Fact] + public void GreaterThan_PhysicalAddress() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count(x => EF.Functions.GreaterThan(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); + + Assert.Equal(2, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr" > MACADDR '123456000007' +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void GreaterThan_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var count = context.NetTestEntities.Count( + x => EF.Functions.GreaterThan(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); + + Assert.Equal(2, count); + AssertSql( + """ +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Macaddr8" > MACADDR8 '08002B0102030407' +"""); + } + + #endregion + + #region ContainmentOperatorTests + + [Fact] + public void ContainedBy_IPAddress_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainedBy(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" << @p +"""); + } + + [Fact] + public void ContainedBy_IPAddress_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainedBy(x.Inet, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" << @p +"""); + } + + [Fact] + public void ContainedBy_GaussDBCidr_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainedBy(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" << @p +"""); + } + + [Fact] + public void ContainedByOrEqual_IPAddress_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainedByOrEqual(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" <<= @p +"""); + } + + [Fact] + public void ContainedByOrEqual_IPAddress_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainedByOrEqual(x.Inet, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" <<= @p +"""); + } + + [Fact] + public void ContainedByOrEqual_GaussDBCidr_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainedByOrEqual(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" <<= @p +"""); + } + + [Fact] + public void Contains_IPAddress_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.Contains(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" >> @p +"""); + } + + [Fact] + public void Contains_GaussDBCidr_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.Contains(x.Cidr, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" >> @p +"""); + } + + [Fact] + public void Contains_GaussDBCidr_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.Contains(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" >> @p +"""); + } + + [Fact] + public void ContainsOrEqual_IPAddress_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrEqual(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" >>= @p +"""); + } + + [Fact] + public void ContainsOrEqual_GaussDBCidr_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrEqual(x.Cidr, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" >>= @p +"""); + } + + [Fact] + public void ContainsOrEqual_GaussDBCidr_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrEqual(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" >>= @p +"""); + } + + [Fact] + public void ContainsOrContainedBy_IPAddress_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrContainedBy(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" && @p +"""); + } + + [Fact] + public void ContainsOrContainedBy_IPAddress_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrContainedBy(x.Inet, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" && @p +"""); + } + + [Fact] + public void ContainsOrContainedBy_GaussDBCidr_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrContainedBy(x.Cidr, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" && @p +"""); + } + + [Fact] + public void ContainsOrContainedBy_GaussDBCidr_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Where(x => EF.Functions.ContainsOrContainedBy(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Cidr" && @p +"""); + } + + #endregion + + #region BitwiseOperatorTests + + [Fact] + public void BitwiseNot_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseNot(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT ~n."Inet" +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseNot_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseNot(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT ~n."Cidr" +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseNot_PhysicalAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseNot(x.Macaddr)) + .ToArray(); + + AssertSql( + """ +SELECT ~n."Macaddr" +FROM "NetTestEntities" AS n +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void BitwiseNot_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseNot(x.Macaddr8)) + .ToArray(); + + AssertSql( + """ +SELECT ~n."Macaddr8" +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseAnd_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var count = context.NetTestEntities.Count(x => x.Inet == EF.Functions.BitwiseAnd(x.Inet, inet)); + + Assert.Equal(0, count); + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT count(*)::int +FROM "NetTestEntities" AS n +WHERE n."Inet" = n."Inet" & @p +"""); + } + + [Fact] + public void BitwiseAnd_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseAnd(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Cidr" & @p +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseAnd_PhysicalAddress() + { + using var context = CreateContext(); + var macaddr = new PhysicalAddress(new byte[6]); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseAnd(x.Macaddr, macaddr)) + .ToArray(); + + AssertSql( + """ +@macaddr='000000000000' (DbType = Object) + +SELECT n."Macaddr" & @macaddr +FROM "NetTestEntities" AS n +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void BitwiseAnd_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseAnd(x.Macaddr8, x.Macaddr8)) + .ToArray(); + + AssertSql( + """ +SELECT n."Macaddr8" & n."Macaddr8" +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseOr_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseOr(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Inet" | @p +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseOr_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseOr(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Cidr" | @p +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void BitwiseOr_PhysicalAddress() + { + using var context = CreateContext(); + var macaddr = new PhysicalAddress(new byte[6]); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseOr(x.Macaddr, macaddr)) + .ToArray(); + + AssertSql( + """ +@macaddr='000000000000' (DbType = Object) + +SELECT n."Macaddr" | @macaddr +FROM "NetTestEntities" AS n +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void BitwiseOr_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.BitwiseOr(x.Macaddr8, x.Macaddr8)) + .ToArray(); + + AssertSql( + """ +SELECT n."Macaddr8" | n."Macaddr8" +FROM "NetTestEntities" AS n +"""); + } + + #endregion + + #region ArithmeticOperatorTests + + [Fact] + public void Add_IPAddress_and_int() + { + using var context = CreateContext(); + var actual = context.NetTestEntities.Single(x => EF.Functions.Add(x.Inet, 1) == IPAddress.Parse("192.168.1.2")).Inet; + + Assert.Equal(actual, IPAddress.Parse("192.168.1.1")); + AssertSql( + """ +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" + 1 = INET '192.168.1.2' +LIMIT 2 +"""); + } + + [Fact] + public void Add_GaussDBCidr_and_int() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Add(x.Cidr, 1)) + .ToArray(); + + AssertSql( + """ +SELECT n."Cidr" + 1 +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Subtract_IPAddress_and_int() + { + using var context = CreateContext(); + var actual = context.NetTestEntities.Single(x => EF.Functions.Subtract(x.Inet, 1) == IPAddress.Parse("192.168.1.1")).Inet; + + Assert.Equal(actual, IPAddress.Parse("192.168.1.2")); + AssertSql( + """ +SELECT n."Id", n."Cidr", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" +FROM "NetTestEntities" AS n +WHERE n."Inet" - 1 = INET '192.168.1.1' +LIMIT 2 +"""); + } + + [Fact] + public void Subtract_GaussDBCidr_and_int() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Subtract(x.Cidr, 1)) + .ToArray(); + + AssertSql( + """ +SELECT n."Cidr" - 1 +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Subtract_IPAddress_and_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Select(x => EF.Functions.Subtract(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT n."Inet" - @p +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Subtract_GaussDBCidr_and_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Subtract(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT n."Cidr" - @p +FROM "NetTestEntities" AS n +"""); + } + + #endregion + + #region FunctionTests + + [Fact] + public void Abbreviate_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Abbreviate(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT abbrev(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Abbreviate_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Abbreviate(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT abbrev(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Broadcast_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Broadcast(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT broadcast(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Broadcast_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Broadcast(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT broadcast(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Family_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Family(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT family(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Family_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Family(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT family(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Host_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Host(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT host(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Host_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Host(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT host(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void HostMask_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.HostMask(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT hostmask(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void HostMask_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.HostMask(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT hostmask(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void MaskLength_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.MaskLength(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT masklen(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void MaskLength_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.MaskLength(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT masklen(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Netmask_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Netmask(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT netmask(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Netmask_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Netmask(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT netmask(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Network_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Network(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT network(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Network_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Network(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT network(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void SetMaskLength_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.SetMaskLength(x.Inet, default)) + .ToArray(); + + AssertSql( + """ +SELECT set_masklen(n."Inet", 0) +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void SetMaskLength_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.SetMaskLength(x.Cidr, default)) + .ToArray(); + + AssertSql( + """ +SELECT set_masklen(n."Cidr", 0) +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Text_IPAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Text(x.Inet)) + .ToArray(); + + AssertSql( + """ +SELECT text(n."Inet") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Text_GaussDBCidr() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Text(x.Cidr)) + .ToArray(); + + AssertSql( + """ +SELECT text(n."Cidr") +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void SameFamily_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Select(x => EF.Functions.SameFamily(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT inet_same_family(n."Inet", @p) +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void SameFamily_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Select(x => EF.Functions.SameFamily(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT inet_same_family(n."Cidr", @p) +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Merge_IPAddress() + { + using var context = CreateContext(); + var inet = IPAddress.Any; + var _ = context.NetTestEntities + .Select(x => EF.Functions.Merge(x.Inet, inet)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0' (DbType = Object) + +SELECT inet_merge(n."Inet", @p) +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Merge_GaussDBCidr() + { + using var context = CreateContext(); + var cidr = new GaussDBCidr(IPAddress.Any, default); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Merge(x.Cidr, cidr)) + .ToArray(); + + AssertSql( + """ +@p='0.0.0.0/0' (DbType = Object) + +SELECT inet_merge(n."Cidr", @p) +FROM "NetTestEntities" AS n +"""); + } + + [Fact] + public void Truncate_PhysicalAddress() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Truncate(x.Macaddr)) + .ToArray(); + + AssertSql( + """ +SELECT trunc(n."Macaddr") +FROM "NetTestEntities" AS n +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void Truncate_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Truncate(x.Macaddr8)) + .ToArray(); + + AssertSql( + """ +SELECT trunc(n."Macaddr8") +FROM "NetTestEntities" AS n +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void Set7BitMac8_PhysicalAddress_macaddr8() + { + using var context = CreateContext(); + var _ = context.NetTestEntities + .Select(x => EF.Functions.Set7BitMac8(x.Macaddr8)) + .ToArray(); + + AssertSql( + """ +SELECT macaddr8_set7bit(n."Macaddr8") +FROM "NetTestEntities" AS n +"""); + } + + #endregion + + #region Fixtures + + /// + /// Represents a fixture suitable for testing network address operators. + /// + public class NetworkAddressQueryGaussDBFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "NetworkQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override Task SeedAsync(NetContext context) + => NetContext.SeedAsync(context); + } + + /// + /// Represents an entity suitable for testing network address operators. + /// + public class NetTestEntity + { + // ReSharper disable once UnusedMember.Global + /// + /// The primary key. + /// + [Key] + public int Id { get; set; } + + /// + /// The network address. + /// + public IPAddress Inet { get; set; } = null!; + + /// + /// The network address. + /// + public GaussDBCidr Cidr { get; set; } + + /// + /// The MAC address. + /// + public PhysicalAddress Macaddr { get; set; } = null!; + + /// + /// The MAC address. + /// + [Column(TypeName = "macaddr8")] + public PhysicalAddress Macaddr8 { get; set; } = null!; + + /// + /// The text form of . + /// + public string TextInet { get; set; } = null!; + + /// + /// The text form of . + /// + public string TextMacaddr { get; set; } = null!; + } + + /// + /// Represents a database suitable for testing network address operators. + /// + public class NetContext : PoolableDbContext + { + /// + /// Represents a set of entities with properties. + /// + public DbSet NetTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public NetContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + if (TestEnvironment.PostgresVersion < new Version(10, 0)) + { + modelBuilder.Entity().Ignore(x => x.Macaddr8); + } + + base.OnModelCreating(modelBuilder); + } + + public static async Task SeedAsync(NetContext context) + { + for (var i = 1; i <= 9; i++) + { + var ip = IPAddress.Parse("192.168.1." + i); + var macaddr = PhysicalAddress.Parse("12-34-56-00-00-0" + i); + var macaddr8 = PhysicalAddress.Parse("08-00-2B-01-02-03-04-0" + i); + context.NetTestEntities.Add( + new NetTestEntity + { + Id = i, + Inet = ip, + Cidr = new GaussDBCidr(IPAddress.Parse("192.168.1.0"), 24), + Macaddr = macaddr, + Macaddr8 = macaddr8, + TextInet = ip.ToString(), + TextMacaddr = macaddr.ToString() + }); + } + + await context.SaveChangesAsync(); + } + } + + #endregion + + #region Helpers + + protected NetContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + #endregion +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs index 35fc4cf586..84219d7153 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class DateIntervalTranslationsTest : QueryTestBase +public class DateIntervalTranslationsTest : QueryTestBase { - public DateIntervalTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public DateIntervalTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); @@ -158,7 +158,7 @@ await AssertQuery( } [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in GaussDB 14 [MemberData(nameof(IsAsyncData))] public async Task RangeAgg(bool async) { @@ -187,7 +187,7 @@ LIMIT 2 } [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in GaussDB 14 [MemberData(nameof(IsAsyncData))] public async Task Intersect_aggregate(bool async) { diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs index 57c1e06dee..f8e7a5ece5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class DurationTranslationsTest : QueryTestBase +public class DurationTranslationsTest : QueryTestBase { - public DurationTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public DurationTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs index 8081f36d89..692dfc2ac3 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class InstantTranslationsTest : QueryTestBase +public class InstantTranslationsTest : QueryTestBase { - public InstantTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public InstantTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs index 5a897eb5af..193078548d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class IntervalTranslationsTest : QueryTestBase +public class IntervalTranslationsTest : QueryTestBase { - public IntervalTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public IntervalTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); @@ -116,7 +116,7 @@ WHERE @interval @> n."Instant" } [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in GaussDB 14 [MemberData(nameof(IsAsyncData))] public async Task Interval_RangeAgg(bool async) { @@ -146,7 +146,7 @@ LIMIT 2 } [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in GaussDB 14 [MemberData(nameof(IsAsyncData))] public async Task Interval_Intersect_aggregate(bool async) { diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs index 212c0a558b..743edb1cf3 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class LocalDateTimeTranslationsTest : QueryTestBase +public class LocalDateTimeTranslationsTest : QueryTestBase { - public LocalDateTimeTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public LocalDateTimeTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs index b0b7055839..be27120999 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class LocalDateTranslationsTest : QueryTestBase +public class LocalDateTranslationsTest : QueryTestBase { - public LocalDateTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public LocalDateTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs similarity index 95% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs index f5399b1c54..e0ecebbca9 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class LocalTimeTranslationsTest : QueryTestBase +public class LocalTimeTranslationsTest : QueryTestBase { - public LocalTimeTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public LocalTimeTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs similarity index 82% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs index 2903114543..20d1c643f7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs @@ -1,21 +1,21 @@ using Microsoft.EntityFrameworkCore.TestModels.NodaTime; using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class NodaTimeQueryNpgsqlFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory +public class NodaTimeQueryGaussDBFixture : 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 + // Set the GaussDB 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. + // We also instruct the test store to pass a connection string to UseGaussDB() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an GaussDBDataSource internally. protected override ITestStoreFactory TestStoreFactory - => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin", useConnectionString: true); + => new GaussDBTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin", useConnectionString: true); public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; @@ -23,12 +23,12 @@ public TestSqlLoggerFactory TestSqlLoggerFactory private NodaTimeData? _expectedData; protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNodaTime(); + => base.AddServices(serviceCollection).AddEntityFrameworkGaussDBNodaTime(); public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); + new GaussDBDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); return optionsBuilder; } diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs index dda9e79a17..e61baee1a5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class PeriodTranslationsTest : QueryTestBase +public class PeriodTranslationsTest : QueryTestBase { - public PeriodTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public PeriodTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); @@ -108,7 +108,7 @@ WHERE floor(date_part('second', n."Period"))::int = 23 """); } - // PostgreSQL does not support extracting weeks from intervals + // GaussDB does not support extracting weeks from intervals [ConditionalFact] public Task Weeks_is_not_translated() { diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs index 6811b15bc7..ffd3a19377 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs @@ -3,9 +3,9 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; -public class ZonedDateTimeTranslationsTest : QueryTestBase +public class ZonedDateTimeTranslationsTest : QueryTestBase { - public ZonedDateTimeTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public ZonedDateTimeTranslationsTest(NodaTimeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/ArithmeticOperatorTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/ArithmeticOperatorTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..04916b8bb8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/ArithmeticOperatorTranslationsGaussDBTest.cs @@ -0,0 +1,78 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; + +public class ArithmeticOperatorTranslationsGaussDBTest : ArithmeticOperatorTranslationsTestBase +{ + public ArithmeticOperatorTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Add(bool async) + { + await base.Add(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" + 2 = 10 +"""); + } + + public override async Task Subtract(bool async) + { + await base.Subtract(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" - 3 = 5 +"""); + } + + public override async Task Multiply(bool async) + { + await base.Multiply(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" * 2 = 16 +"""); + } + + public override async Task Modulo(bool async) + { + await base.Modulo(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" % 3 = 2 +"""); + } + + public override async Task Minus(bool async) + { + await base.Minus(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE -b."Int" = -8 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..05a4b28ade --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsGaussDBTest.cs @@ -0,0 +1,205 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; + +public class BitwiseOperatorTranslationsGaussDBTest : BitwiseOperatorTranslationsTestBase +{ + public BitwiseOperatorTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Or(bool async) + { + await base.Or(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::bigint | b."Long" = 7 +""", + // + """ +SELECT b."Int"::bigint | b."Long" +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Or_over_boolean(bool async) + { + await base.Or_over_boolean(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 12 OR b."String" = 'Seattle' +""", + // + """ +SELECT b."Int" = 12 OR b."String" = 'Seattle' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Or_multiple(bool async) + { + await base.Or_multiple(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."Int" | b."Short" AS bigint) | b."Long" = 7 +"""); + } + + public override async Task And(bool async) + { + await base.And(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" & b."Short" = 2 +""", + // + """ +SELECT b."Int" & b."Short" +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task And_over_boolean(bool async) + { + await base.And_over_boolean(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 8 AND b."String" = 'Seattle' +""", + // + """ +SELECT b."Int" = 8 AND b."String" = 'Seattle' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Xor(bool async) + { + await base.Xor(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE (b."Int" # b."Short") = 1 +""", + // + """ +SELECT b."Int" # b."Short" +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Xor_over_boolean(bool async) + { + await base.Xor_over_boolean(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE (b."Int" = b."Short") <> (b."String" = 'Seattle') +"""); + } + + public override async Task Complement(bool async) + { + await base.Complement(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE ~b."Int" = -9 +"""); + } + + public override async Task And_or_over_boolean(bool async) + { + await base.And_or_over_boolean(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE (b."Int" = 12 AND b."Short" = 12) OR b."String" = 'Seattle' +"""); + } + + public override async Task Or_with_logical_or(bool async) + { + await base.Or_with_logical_or(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 12 OR b."Short" = 12 OR b."String" = 'Seattle' +"""); + } + + public override async Task And_with_logical_and(bool async) + { + await base.And_with_logical_and(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 8 AND b."Short" = 8 AND b."String" = 'Seattle' +"""); + } + + public override async Task Or_with_logical_and(bool async) + { + await base.Or_with_logical_and(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE (b."Int" = 8 OR b."Short" = 9) AND b."String" = 'Seattle' +"""); + } + + public override async Task And_with_logical_or(bool async) + { + await base.And_with_logical_or(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE (b."Int" = 12 AND b."Short" = 12) OR b."String" = 'Seattle' +"""); + } + + public override Task Left_shift(bool async) + => AssertTranslationFailed(() => base.Left_shift(async)); + + public override Task Right_shift(bool async) + => AssertTranslationFailed(() => base.Right_shift(async)); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/ComparisonOperatorTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/ComparisonOperatorTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..df9aa32085 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/ComparisonOperatorTranslationsGaussDBTest.cs @@ -0,0 +1,90 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; + +public class ComparisonOperatorTranslationsGaussDBTest : ComparisonOperatorTranslationsTestBase +{ + public ComparisonOperatorTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Equal(bool async) + { + await base.Equal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 8 +"""); + } + + public override async Task NotEqual(bool async) + { + await base.NotEqual(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" <> 8 +"""); + } + + public override async Task GreaterThan(bool async) + { + await base.GreaterThan(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" > 8 +"""); + } + + public override async Task GreaterThanOrEqual(bool async) + { + await base.GreaterThanOrEqual(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" >= 8 +"""); + } + + public override async Task LessThan(bool async) + { + await base.LessThan(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" < 8 +"""); + } + + public override async Task LessThanOrEqual(bool async) + { + await base.LessThanOrEqual(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" <= 8 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/LogicalOperatorTranslationsGaussDBlTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/LogicalOperatorTranslationsGaussDBlTest.cs new file mode 100644 index 0000000000..adaba769b2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/LogicalOperatorTranslationsGaussDBlTest.cs @@ -0,0 +1,90 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; + +public class LogicalOperatorTranslationsGaussDBlTest : LogicalOperatorTranslationsTestBase +{ + public LogicalOperatorTranslationsGaussDBlTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task And(bool async) + { + await base.And(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 8 AND b."String" = 'Seattle' +"""); + } + + public override async Task And_with_bool_property(bool async) + { + await base.And_with_bool_property(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Bool" AND b."String" = 'Seattle' +"""); + } + + public override async Task Or(bool async) + { + await base.Or(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" = 999 OR b."String" = 'Seattle' +"""); + } + + public override async Task Or_with_bool_property(bool async) + { + await base.Or_with_bool_property(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Bool" OR b."String" = 'Seattle' +"""); + } + + public override async Task Not(bool async) + { + await base.Not(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int" <> 999 +"""); + } + + public override async Task Not_with_bool_property(bool async) + { + await base.Not_with_bool_property(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE NOT (b."Bool") +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..5b900ebed2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsGaussDBTest.cs @@ -0,0 +1,45 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; + +public class MiscellaneousOperatorTranslationsGaussDBTest : MiscellaneousOperatorTranslationsTestBase +{ + public MiscellaneousOperatorTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Conditional(bool async) + { + await base.Conditional(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CASE + WHEN b."Int" = 8 THEN b."String" + ELSE 'Foo' +END = 'Seattle' +"""); + } + + public override async Task Coalesce(bool async) + { + await base.Coalesce(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE COALESCE(n."String", 'Unknown') = 'Seattle' +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/RangeTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/RangeTranslationsTest.cs similarity index 85% rename from test/EFCore.PG.FunctionalTests/Query/Translations/RangeTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/RangeTranslationsTest.cs index 9c5d53b176..64dd24b60c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/RangeTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/RangeTranslationsTest.cs @@ -1,17 +1,17 @@ using System.ComponentModel.DataAnnotations.Schema; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.Query.Translations; // Note: timestamp range tests are in TimestampQueryTest -public class RangeTranslationsTest : IClassFixture +public class RangeTranslationsTest : IClassFixture { - private RangeQueryNpgsqlFixture Fixture { get; } + private RangeQueryGaussDBFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public RangeTranslationsTest(RangeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public RangeTranslationsTest(RangeQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); @@ -41,7 +41,7 @@ public void Contains_range() { using var context = CreateContext(); - var range = new NpgsqlRange(8, 13); + var range = new GaussDBRange(8, 13); var result = context.RangeTestEntities.Single(x => x.IntRange.Contains(range)); Assert.Equal(2, result.Id); @@ -60,7 +60,7 @@ LIMIT 2 public void ContainedBy() { using var context = CreateContext(); - var range = new NpgsqlRange(8, 13); + var range = new GaussDBRange(8, 13); var result = context.RangeTestEntities.Single(x => range.ContainedBy(x.IntRange)); Assert.Equal(2, result.Id); @@ -79,7 +79,7 @@ LIMIT 2 public void Equals_operator() { using var context = CreateContext(); - var range = new NpgsqlRange(1, 10); + var range = new GaussDBRange(1, 10); var result = context.RangeTestEntities.Single(x => x.IntRange == range); Assert.Equal(1, result.Id); @@ -98,7 +98,7 @@ LIMIT 2 public void Equals_method() { using var context = CreateContext(); - var range = new NpgsqlRange(1, 10); + var range = new GaussDBRange(1, 10); var result = context.RangeTestEntities.Single(x => x.IntRange.Equals(range)); Assert.Equal(1, result.Id); @@ -117,7 +117,7 @@ LIMIT 2 public void Overlaps_range() { using var context = CreateContext(); - var range = new NpgsqlRange(-5, 4); + var range = new GaussDBRange(-5, 4); var result = context.RangeTestEntities.Single(x => x.IntRange.Overlaps(range)); Assert.Equal(1, result.Id); @@ -136,7 +136,7 @@ LIMIT 2 public void IsStrictlyLeftOf_range() { using var context = CreateContext(); - var range = new NpgsqlRange(11, 15); + var range = new GaussDBRange(11, 15); var result = context.RangeTestEntities.Single(x => x.IntRange.IsStrictlyLeftOf(range)); Assert.Equal(1, result.Id); @@ -155,7 +155,7 @@ LIMIT 2 public void IsStrictlyRightOf_range() { using var context = CreateContext(); - var range = new NpgsqlRange(0, 4); + var range = new GaussDBRange(0, 4); var result = context.RangeTestEntities.Single(x => x.IntRange.IsStrictlyRightOf(range)); Assert.Equal(2, result.Id); @@ -174,7 +174,7 @@ LIMIT 2 public void DoesNotExtendLeftOf() { using var context = CreateContext(); - var range = new NpgsqlRange(2, 20); + var range = new GaussDBRange(2, 20); var result = context.RangeTestEntities.Single(x => range.DoesNotExtendLeftOf(x.IntRange)); Assert.Equal(1, result.Id); @@ -193,7 +193,7 @@ LIMIT 2 public void DoesNotExtendRightOf() { using var context = CreateContext(); - var range = new NpgsqlRange(1, 13); + var range = new GaussDBRange(1, 13); var result = context.RangeTestEntities.Single(x => range.DoesNotExtendRightOf(x.IntRange)); Assert.Equal(2, result.Id); @@ -212,7 +212,7 @@ LIMIT 2 public void IsAdjacentTo() { using var context = CreateContext(); - var range = new NpgsqlRange(2, 4); + var range = new GaussDBRange(2, 4); var result = context.RangeTestEntities.Single(x => range.IsAdjacentTo(x.IntRange)); Assert.Equal(2, result.Id); @@ -231,8 +231,8 @@ LIMIT 2 public void Union() { using var context = CreateContext(); - var range = new NpgsqlRange(-2, 7); - var result = context.RangeTestEntities.Single(x => x.IntRange.Union(range) == new NpgsqlRange(-2, 10)); + var range = new GaussDBRange(-2, 7); + var result = context.RangeTestEntities.Single(x => x.IntRange.Union(range) == new GaussDBRange(-2, 10)); Assert.Equal(1, result.Id); AssertSql( @@ -247,7 +247,7 @@ LIMIT 2 } [ConditionalFact] - [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in GaussDB 14 public void Union_aggregate() { using var context = CreateContext(); @@ -277,8 +277,8 @@ LIMIT 2 public void Intersect() { using var context = CreateContext(); - var range = new NpgsqlRange(-2, 3); - var result = context.RangeTestEntities.Single(x => x.IntRange.Intersect(range) == new NpgsqlRange(1, 3)); + var range = new GaussDBRange(-2, 3); + var result = context.RangeTestEntities.Single(x => x.IntRange.Intersect(range) == new GaussDBRange(1, 3)); Assert.Equal(1, result.Id); AssertSql( @@ -293,7 +293,7 @@ LIMIT 2 } [ConditionalFact] - [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in GaussDB 14 public void Intersect_aggregate() { using var context = CreateContext(); @@ -304,7 +304,7 @@ public void Intersect_aggregate() .Select(g => g.Select(x => x.IntRange).RangeIntersectAgg()) .Single(); - Assert.Equal(new NpgsqlRange(5, true, 11, false), intersection); + Assert.Equal(new GaussDBRange(5, true, 11, false), intersection); AssertSql( """ @@ -323,8 +323,8 @@ LIMIT 2 public void Except() { using var context = CreateContext(); - var range = new NpgsqlRange(1, 2); - var result = context.RangeTestEntities.Single(x => x.IntRange.Except(range) == new NpgsqlRange(3, 10)); + var range = new GaussDBRange(1, 2); + var result = context.RangeTestEntities.Single(x => x.IntRange.Except(range) == new GaussDBRange(3, 10)); Assert.Equal(1, result.Id); AssertSql( @@ -378,7 +378,7 @@ LIMIT 2 public void IsEmpty() { using var context = CreateContext(); - var result = context.RangeTestEntities.Single(x => x.IntRange.Intersect(new NpgsqlRange(1, 2)).IsEmpty); + var result = context.RangeTestEntities.Single(x => x.IntRange.Intersect(new GaussDBRange(1, 2)).IsEmpty); Assert.Equal(2, result.Id); AssertSql( @@ -454,7 +454,7 @@ WHERE upper_inf(r."IntRange") public void Merge() { using var context = CreateContext(); - var result = context.RangeTestEntities.Single(x => x.IntRange.Merge(new NpgsqlRange(12, 13)) == new NpgsqlRange(1, 13)); + var result = context.RangeTestEntities.Single(x => x.IntRange.Merge(new GaussDBRange(12, 13)) == new GaussDBRange(1, 13)); Assert.Equal(1, result.Id); AssertSql( @@ -606,13 +606,13 @@ LIMIT 2 #region Fixtures - public class RangeQueryNpgsqlFixture : SharedStoreFixtureBase + public class RangeQueryGaussDBFixture : SharedStoreFixtureBase { - static RangeQueryNpgsqlFixture() + static RangeQueryGaussDBFixture() { - // TODO: Switch to using NpgsqlDataSource + // TODO: Switch to using GaussDBDataSource #pragma warning disable CS0618 // Type or member is obsolete - NpgsqlConnection.GlobalTypeMapper.EnableUnmappedTypes(); + GaussDBConnection.GlobalTypeMapper.EnableUnmappedTypes(); #pragma warning restore CS0618 // Type or member is obsolete } @@ -620,7 +620,7 @@ protected override string StoreName => "RangeQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; @@ -631,7 +631,7 @@ protected override Task SeedAsync(RangeContext context) public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { var optionsBuilder = base.AddOptions(builder); - var npgsqlOptionsBuilder = new NpgsqlDbContextOptionsBuilder(optionsBuilder); + var npgsqlOptionsBuilder = new GaussDBDbContextOptionsBuilder(optionsBuilder); npgsqlOptionsBuilder.MapRange("doublerange", typeof(double)); npgsqlOptionsBuilder.MapRange("Schema_Range", "test"); return optionsBuilder; @@ -641,16 +641,16 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build public class RangeTestEntity { public int Id { get; set; } - public NpgsqlRange IntRange { get; set; } - public NpgsqlRange LongRange { get; set; } - public NpgsqlRange DecimalRange { get; set; } - public NpgsqlRange DateOnlyDateRange { get; set; } + public GaussDBRange IntRange { get; set; } + public GaussDBRange LongRange { get; set; } + public GaussDBRange DecimalRange { get; set; } + public GaussDBRange DateOnlyDateRange { get; set; } [Column(TypeName = "tsrange")] - public NpgsqlRange DateTimeDateRange { get; set; } + public GaussDBRange DateTimeDateRange { get; set; } - public NpgsqlRange UserDefinedRange { get; set; } - public NpgsqlRange UserDefinedRangeWithSchema { get; set; } + public GaussDBRange UserDefinedRange { get; set; } + public GaussDBRange UserDefinedRangeWithSchema { get; set; } } public class RangeContext(DbContextOptions options) : PoolableDbContext(options) @@ -667,24 +667,24 @@ public static async Task SeedAsync(RangeContext context) new RangeTestEntity { Id = 1, - IntRange = new NpgsqlRange(1, 10), - LongRange = new NpgsqlRange(1, 10), - DecimalRange = new NpgsqlRange(1, 10), - DateOnlyDateRange = new NpgsqlRange(new DateOnly(2020, 1, 1), new DateOnly(2020, 1, 10)), - DateTimeDateRange = new NpgsqlRange(new DateTime(2020, 1, 1), new DateTime(2020, 1, 10)), - UserDefinedRange = new NpgsqlRange(1, 10), - UserDefinedRangeWithSchema = new NpgsqlRange(1, 10) + IntRange = new GaussDBRange(1, 10), + LongRange = new GaussDBRange(1, 10), + DecimalRange = new GaussDBRange(1, 10), + DateOnlyDateRange = new GaussDBRange(new DateOnly(2020, 1, 1), new DateOnly(2020, 1, 10)), + DateTimeDateRange = new GaussDBRange(new DateTime(2020, 1, 1), new DateTime(2020, 1, 10)), + UserDefinedRange = new GaussDBRange(1, 10), + UserDefinedRangeWithSchema = new GaussDBRange(1, 10) }, new RangeTestEntity { Id = 2, - IntRange = new NpgsqlRange(5, 15), - LongRange = new NpgsqlRange(5, 15), - DecimalRange = new NpgsqlRange(5, 15), - DateOnlyDateRange = new NpgsqlRange(new DateOnly(2020, 1, 5), new DateOnly(2020, 1, 15)), - DateTimeDateRange = new NpgsqlRange(new DateTime(2020, 1, 5), new DateTime(2020, 1, 15)), - UserDefinedRange = new NpgsqlRange(5, 15), - UserDefinedRangeWithSchema = new NpgsqlRange(5, 15) + IntRange = new GaussDBRange(5, 15), + LongRange = new GaussDBRange(5, 15), + DecimalRange = new GaussDBRange(5, 15), + DateOnlyDateRange = new GaussDBRange(new DateOnly(2020, 1, 5), new DateOnly(2020, 1, 15)), + DateTimeDateRange = new GaussDBRange(new DateTime(2020, 1, 5), new DateTime(2020, 1, 15)), + UserDefinedRange = new GaussDBRange(5, 15), + UserDefinedRangeWithSchema = new GaussDBRange(5, 15) }); await context.SaveChangesAsync(); diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/StringTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/StringTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..02a233db57 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/StringTranslationsGaussDBTest.cs @@ -0,0 +1,1573 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +public class StringTranslationsGaussDBTest : StringTranslationsRelationalTestBase +{ + public StringTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Equals + + public override async Task Equals(bool async) + { + await base.Equals(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'Seattle' +"""); + } + + public override async Task Equals_with_OrdinalIgnoreCase(bool async) + { + await base.Equals_with_OrdinalIgnoreCase(async); + + AssertSql(); + } + + public override async Task Equals_with_Ordinal(bool async) + { + await base.Equals_with_Ordinal(async); + + AssertSql(); + } + + public override async Task Static_Equals(bool async) + { + await base.Static_Equals(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'Seattle' +"""); + } + + public override async Task Static_Equals_with_OrdinalIgnoreCase(bool async) + { + await base.Static_Equals_with_OrdinalIgnoreCase(async); + + AssertSql(); + } + + public override async Task Static_Equals_with_Ordinal(bool async) + { + await base.Static_Equals_with_Ordinal(async); + + AssertSql(); + } + + #endregion Equals + + #region Miscellaneous + + public override async Task Length(bool async) + { + await base.Length(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int = 7 +"""); + } + + public override async Task ToUpper(bool async) + { + await base.ToUpper(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE upper(b."String") = 'SEATTLE' +""", + // + """ +SELECT upper(b."String") +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task ToLower(bool async) + { + await base.ToLower(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE lower(b."String") = 'seattle' +""", + // + """ +SELECT lower(b."String") +FROM "BasicTypesEntities" AS b +"""); + } + + #endregion Miscellaneous + + #region IndexOf + + public override async Task IndexOf(bool async) + { + await base.IndexOf(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos(b."String", 'eattl') - 1 <> -1 +"""); + } + + // TODO: #3547 + public override Task IndexOf_Char(bool async) + => Assert.ThrowsAsync(() => base.IndexOf_Char(async)); + + public override async Task IndexOf_with_empty_string(bool async) + { + await base.IndexOf_with_empty_string(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos(b."String", '') - 1 = 0 +"""); + } + + public override async Task IndexOf_with_one_parameter_arg(bool async) + { + await base.IndexOf_with_one_parameter_arg(async); + + AssertSql( + """ +@pattern='eattl' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos(b."String", @pattern) - 1 = 1 +"""); + } + + public override async Task IndexOf_with_one_parameter_arg_char(bool async) + { + await base.IndexOf_with_one_parameter_arg_char(async); + + AssertSql( + """ +@pattern='e' (DbType = String) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos(b."String", @pattern) - 1 = 1 +"""); + } + + // GaussDB does not have strpos with starting position + public override Task IndexOf_with_constant_starting_position(bool async) + => AssertTranslationFailed(() => base.IndexOf_with_constant_starting_position(async)); + + // GaussDB does not have strpos with starting position + public override Task IndexOf_with_constant_starting_position_char(bool async) + => AssertTranslationFailed(() => base.IndexOf_with_constant_starting_position_char(async)); + + // GaussDB does not have strpos with starting position + public override Task IndexOf_with_parameter_starting_position(bool async) + => AssertTranslationFailed(() => base.IndexOf_with_parameter_starting_position(async)); + + // GaussDB does not have strpos with starting position + public override Task IndexOf_with_parameter_starting_position_char(bool async) + => AssertTranslationFailed(() => base.IndexOf_with_parameter_starting_position_char(async)); + + public override async Task IndexOf_after_ToString(bool async) + { + await base.IndexOf_after_ToString(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos(b."Int"::text, '55') - 1 = 1 +"""); + } + + public override async Task IndexOf_over_ToString(bool async) + { + await base.IndexOf_over_ToString(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos('12559', b."Int"::text) - 1 = 1 +"""); + } + + #endregion IndexOf + + #region Replace + + public override async Task Replace(bool async) + { + await base.Replace(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE replace(b."String", 'Sea', 'Rea') = 'Reattle' +"""); + } + + // TODO: #3547 + public override Task Replace_Char(bool async) + => AssertTranslationFailed(() => base.Replace_Char(async)); + + public override async Task Replace_with_empty_string(bool async) + { + await base.Replace_with_empty_string(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <> '' AND replace(b."String", b."String", '') = '' +"""); + } + + public override async Task Replace_using_property_arguments(bool async) + { + await base.Replace_using_property_arguments(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <> '' AND replace(b."String", b."String", b."Int"::text) = b."Int"::text +"""); + } + + #endregion Replace + + #region Substring + + public override async Task Substring(bool async) + { + await base.Substring(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int >= 3 AND substring(b."String", 2, 2) = 'ea' +"""); + } + + public override async Task Substring_with_one_arg_with_zero_startIndex(bool async) + { + await base.Substring_with_one_arg_with_zero_startIndex(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE substring(b."String", 1) = 'Seattle' +"""); + } + + public override async Task Substring_with_one_arg_with_constant(bool async) + { + await base.Substring_with_one_arg_with_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int >= 1 AND substring(b."String", 2) = 'eattle' +"""); + } + + public override async Task Substring_with_one_arg_with_parameter(bool async) + { + await base.Substring_with_one_arg_with_parameter(async); + + AssertSql( + """ +@start='2' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int >= 2 AND substring(b."String", @start + 1) = 'attle' +"""); + } + + public override async Task Substring_with_two_args_with_zero_startIndex(bool async) + { + await base.Substring_with_two_args_with_zero_startIndex(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int >= 3 AND substring(b."String", 1, 3) = 'Sea' +"""); + } + + public override async Task Substring_with_two_args_with_zero_length(bool async) + { + await base.Substring_with_two_args_with_zero_length(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int >= 2 AND substring(b."String", 3, 0) = '' +"""); + } + + public override async Task Substring_with_two_args_with_parameter(bool async) + { + await base.Substring_with_two_args_with_parameter(async); + + AssertSql( + """ +@start='2' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE length(b."String")::int >= 5 AND substring(b."String", @start + 1, 3) = 'att' +"""); + } + + public override async Task Substring_with_two_args_with_IndexOf(bool async) + { + await base.Substring_with_two_args_with_IndexOf(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE '%a%' AND substring(b."String", (strpos(b."String", 'a') - 1) + 1, 3) = 'att' +"""); + } + + #endregion Substring + + #region IsNullOrEmpty/Whitespace + + public override async Task IsNullOrEmpty(bool async) + { + await base.IsNullOrEmpty(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."String" IS NULL OR n."String" = '' +""", + // + """ +SELECT n."String" IS NULL OR n."String" = '' +FROM "NullableBasicTypesEntities" AS n +"""); + } + + public override async Task IsNullOrEmpty_negated(bool async) + { + await base.IsNullOrEmpty_negated(async); + + AssertSql( + """ +SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" +FROM "NullableBasicTypesEntities" AS n +WHERE n."String" IS NOT NULL AND n."String" <> '' +""", + // + """ +SELECT n."String" IS NOT NULL AND n."String" <> '' +FROM "NullableBasicTypesEntities" AS n +"""); + } + + public override async Task IsNullOrWhiteSpace(bool async) + { + await base.IsNullOrWhiteSpace(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE btrim(b."String", E' \t\n\r') = '' +"""); + } + + #endregion IsNullOrEmpty/Whitespace + + #region StartsWith + + public override async Task StartsWith_Literal(bool async) + { + await base.StartsWith_Literal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE 'Se%' +"""); + } + + // TODO: #3547 + public override Task StartsWith_Literal_Char(bool async) + => AssertTranslationFailed(() => base.StartsWith_Literal_Char(async)); + + public override async Task StartsWith_Parameter(bool async) + { + await base.StartsWith_Parameter(async); + + AssertSql( + """ +@pattern_startswith='Se%' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE @pattern_startswith +"""); + } + + public override Task StartsWith_Parameter_Char(bool async) + => AssertTranslationFailed(() => base.StartsWith_Parameter_Char(async)); + + public override async Task StartsWith_Column(bool async) + { + await base.StartsWith_Column(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE left(b."String", length(b."String")) = b."String" +"""); + } + + public override async Task StartsWith_with_StringComparison_Ordinal(bool async) + { + await base.StartsWith_with_StringComparison_Ordinal(async); + + AssertSql(); + } + + public override async Task StartsWith_with_StringComparison_OrdinalIgnoreCase(bool async) + { + await base.StartsWith_with_StringComparison_OrdinalIgnoreCase(async); + + AssertSql(); + } + + public override async Task StartsWith_with_StringComparison_unsupported(bool async) + { + await base.StartsWith_with_StringComparison_unsupported(async); + + AssertSql(); + } + + #endregion StartsWith + + #region EndsWith + + public override async Task EndsWith_Literal(bool async) + { + await base.EndsWith_Literal(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE '%le' +"""); + } + + // TODO: #3547 + public override Task EndsWith_Literal_Char(bool async) + => AssertTranslationFailed(() => base.EndsWith_Literal_Char(async)); + + public override async Task EndsWith_Parameter(bool async) + { + await base.EndsWith_Parameter(async); + + AssertSql( + """ +@pattern_endswith='%le' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE @pattern_endswith +"""); + } + + // TODO: #3547 + public override Task EndsWith_Parameter_Char(bool async) + => AssertTranslationFailed(() => base.EndsWith_Parameter_Char(async)); + + public override async Task EndsWith_Column(bool async) + { + // SQL Server trims trailing whitespace for length calculations, making our EndsWith() column translation not work reliably in that + // case + await AssertQuery( + async, + ss => ss.Set().Where(b => b.String == "Seattle" && b.String.EndsWith(b.String))); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'Seattle' AND right(b."String", length(b."String")) = b."String" +"""); + } + + public override async Task EndsWith_with_StringComparison_Ordinal(bool async) + { + await base.EndsWith_with_StringComparison_Ordinal(async); + + AssertSql(); + } + + public override async Task EndsWith_with_StringComparison_OrdinalIgnoreCase(bool async) + { + await base.EndsWith_with_StringComparison_OrdinalIgnoreCase(async); + + AssertSql(); + } + + public override async Task EndsWith_with_StringComparison_unsupported(bool async) + { + await base.EndsWith_with_StringComparison_unsupported(async); + + AssertSql(); + } + + #endregion EndsWith + + #region Contains + + public override async Task Contains_Literal(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(c => c.String.Contains("eattl")), // SQL Server is case-insensitive by default + ss => ss.Set().Where(c => c.String.Contains("eattl", StringComparison.OrdinalIgnoreCase))); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE '%eattl%' +"""); + } + + // TODO: #3547 + public override Task Contains_Literal_Char(bool async) + => AssertTranslationFailed(() => base.Contains_Literal_Char(async)); + + public override async Task Contains_Column(bool async) + { + await base.Contains_Column(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE strpos(b."String", b."String") > 0 +""", + // + """ +SELECT strpos(b."String", b."String") > 0 +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Contains_negated(bool async) + { + await base.Contains_negated(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" NOT LIKE '%eattle%' +""", + // + """ +SELECT b."String" NOT LIKE '%eattle%' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task Contains_with_StringComparison_Ordinal(bool async) + { + await base.Contains_with_StringComparison_Ordinal(async); + + AssertSql(); + } + + public override async Task Contains_with_StringComparison_OrdinalIgnoreCase(bool async) + { + await base.Contains_with_StringComparison_OrdinalIgnoreCase(async); + + AssertSql(); + } + + public override async Task Contains_with_StringComparison_unsupported(bool async) + { + await base.Contains_with_StringComparison_unsupported(async); + + AssertSql(); + } + + public override async Task Contains_constant_with_whitespace(bool async) + { + await base.Contains_constant_with_whitespace(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE '% %' +"""); + } + + public override async Task Contains_parameter_with_whitespace(bool async) + { + await base.Contains_parameter_with_whitespace(async); + + AssertSql( + """ +@pattern_contains='% %' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE @pattern_contains +"""); + } + + #endregion Contains + + #region TrimStart + + public override async Task TrimStart_without_arguments(bool async) + { + await base.TrimStart_without_arguments(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE ltrim(b."String", E' \t\n\r') = 'Boston ' +"""); + } + + public override async Task TrimStart_with_char_argument(bool async) + { + await base.TrimStart_with_char_argument(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE ltrim(b."String", 'S') = 'eattle' +"""); + } + + public override async Task TrimStart_with_char_array_argument(bool async) + { + await base.TrimStart_with_char_array_argument(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE ltrim(b."String", 'Se') = 'attle' +"""); + } + + #endregion TrimStart + + #region TrimEnd + + public override async Task TrimEnd_without_arguments(bool async) + { + await base.TrimEnd_without_arguments(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE rtrim(b."String", E' \t\n\r') = ' Boston' +"""); + } + + public override async Task TrimEnd_with_char_argument(bool async) + { + await base.TrimEnd_with_char_argument(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE rtrim(b."String", 'e') = 'Seattl' +"""); + } + + public override async Task TrimEnd_with_char_array_argument(bool async) + { + await base.TrimEnd_with_char_array_argument(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE rtrim(b."String", 'le') = 'Seatt' +"""); + } + + #endregion TrimEnd + + #region Trim + + public override async Task Trim_without_argument_in_predicate(bool async) + { + await base.Trim_without_argument_in_predicate(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE btrim(b."String", E' \t\n\r') = 'Boston' +"""); + } + + public override async Task Trim_with_char_argument_in_predicate(bool async) + { + await base.Trim_with_char_argument_in_predicate(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE btrim(b."String", 'S') = 'eattle' +"""); + } + + public override async Task Trim_with_char_array_argument_in_predicate(bool async) + { + await base.Trim_with_char_array_argument_in_predicate(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE btrim(b."String", 'Se') = 'attl' +"""); + } + + #endregion Trim + + #region Compare + + public override async Task Compare_simple_zero(bool async) + { + await base.Compare_simple_zero(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <> 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +"""); + } + + public override async Task Compare_simple_one(bool async) + { + await base.Compare_simple_one(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" < 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= 'Seattle' +"""); + } + + public override async Task Compare_with_parameter(bool async) + { + await base.Compare_with_parameter(async); + + AssertSql( + """ +@basicTypeEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > @basicTypeEntity_String +""", + // + """ +@basicTypeEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" < @basicTypeEntity_String +""", + // + """ +@basicTypeEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= @basicTypeEntity_String +""", + // + """ +@basicTypeEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= @basicTypeEntity_String +""", + // + """ +@basicTypeEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= @basicTypeEntity_String +""", + // + """ +@basicTypeEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= @basicTypeEntity_String +"""); + } + + public override async Task Compare_simple_more_than_one(bool async) + { + await base.Compare_simple_more_than_one(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CASE + WHEN b."String" = 'Seattle' THEN 0 + WHEN b."String" > 'Seattle' THEN 1 + WHEN b."String" < 'Seattle' THEN -1 +END = 42 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CASE + WHEN b."String" = 'Seattle' THEN 0 + WHEN b."String" > 'Seattle' THEN 1 + WHEN b."String" < 'Seattle' THEN -1 +END > 42 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE 42 > CASE + WHEN b."String" = 'Seattle' THEN 0 + WHEN b."String" > 'Seattle' THEN 1 + WHEN b."String" < 'Seattle' THEN -1 +END +"""); + } + + public override async Task Compare_nested(bool async) + { + await base.Compare_nested(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'M' || b."String" +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <> substring(b."String", 1, 0) +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > replace('Seattle', 'Sea', b."String") +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'M' || b."String" +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > substring(b."String", 1, 0) +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" < replace('Seattle', 'Sea', b."String") +"""); + } + + public override async Task Compare_multi_predicate(bool async) + { + await base.Compare_multi_predicate(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= 'Seattle' AND b."String" < 'Toronto' +"""); + } + + public override async Task CompareTo_simple_zero(bool async) + { + await base.CompareTo_simple_zero(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <> 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +"""); + } + + public override async Task CompareTo_simple_one(bool async) + { + await base.CompareTo_simple_one(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" < 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= 'Seattle' +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= 'Seattle' +"""); + } + + public override async Task CompareTo_with_parameter(bool async) + { + await base.CompareTo_with_parameter(async); + + AssertSql( + """ +@basicTypesEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > @basicTypesEntity_String +""", + // + """ +@basicTypesEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" < @basicTypesEntity_String +""", + // + """ +@basicTypesEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= @basicTypesEntity_String +""", + // + """ +@basicTypesEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= @basicTypesEntity_String +""", + // + """ +@basicTypesEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= @basicTypesEntity_String +""", + // + """ +@basicTypesEntity_String='Seattle' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= @basicTypesEntity_String +"""); + } + + public override async Task CompareTo_simple_more_than_one(bool async) + { + await base.CompareTo_simple_more_than_one(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CASE + WHEN b."String" = 'Seattle' THEN 0 + WHEN b."String" > 'Seattle' THEN 1 + WHEN b."String" < 'Seattle' THEN -1 +END = 42 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CASE + WHEN b."String" = 'Seattle' THEN 0 + WHEN b."String" > 'Seattle' THEN 1 + WHEN b."String" < 'Seattle' THEN -1 +END > 42 +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE 42 > CASE + WHEN b."String" = 'Seattle' THEN 0 + WHEN b."String" > 'Seattle' THEN 1 + WHEN b."String" < 'Seattle' THEN -1 +END +"""); + } + + public override async Task CompareTo_nested(bool async) + { + await base.CompareTo_nested(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" = 'M' || b."String" +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <> substring(b."String", 1, 0) +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > replace('Seattle', 'Sea', b."String") +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" <= 'M' || b."String" +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" > substring(b."String", 1, 0) +""", + // + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" < replace('Seattle', 'Sea', b."String") +"""); + } + + public override async Task Compare_to_multi_predicate(bool async) + { + await base.Compare_to_multi_predicate(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" >= 'Seattle' AND b."String" < 'Toronto' +"""); + } + + #endregion Compare + + #region Join + + public override async Task Join_over_non_nullable_column(bool async) + { + await base.Join_over_non_nullable_column(async); + + AssertSql( + """ +SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", '|'), '') AS "Strings" +FROM "BasicTypesEntities" AS b +GROUP BY b."Int" +"""); + } + + public override async Task Join_over_nullable_column(bool async) + { + await base.Join_over_nullable_column(async); + + AssertSql( + """ +SELECT n0."Key", COALESCE(string_agg(COALESCE(n0."String", ''), '|'), '') AS "Regions" +FROM ( + SELECT n."String", COALESCE(n."Int", 0) AS "Key" + FROM "NullableBasicTypesEntities" AS n +) AS n0 +GROUP BY n0."Key" +"""); + } + + public override async Task Join_with_predicate(bool async) + { + await base.Join_with_predicate(async); + + AssertSql( + """ +SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", '|') FILTER (WHERE length(b."String")::int > 6), '') AS "Strings" +FROM "BasicTypesEntities" AS b +GROUP BY b."Int" +"""); + } + + public override async Task Join_with_ordering(bool async) + { + await base.Join_with_ordering(async); + + AssertSql( + """ +SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", '|' ORDER BY b."Id" DESC NULLS LAST), '') AS "Strings" +FROM "BasicTypesEntities" AS b +GROUP BY b."Int" +"""); + } + + public override async Task Join_non_aggregate(bool async) + { + await base.Join_non_aggregate(async); + + AssertSql( + """ +@foo='foo' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE concat_ws('|', b."String", @foo, '', 'bar') = 'Seattle|foo||bar' +"""); + } + + #endregion Join + + #region Concatenation + + public override async Task Concat_operator(bool async) + { + await base.Concat_operator(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" || 'Boston' = 'SeattleBoston' +"""); + } + + public override async Task Concat_aggregate(bool async) + { + await base.Concat_aggregate(async); + + AssertSql( + """ +SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", ''), '') AS "BasicTypesEntitys" +FROM "BasicTypesEntities" AS b +GROUP BY b."Int" +"""); + } + + public override async Task Concat_string_int_comparison1(bool async) + { + await base.Concat_string_int_comparison1(async); + + AssertSql( + """ +@i='10' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" || @i::text = 'Seattle10' +"""); + } + + public override async Task Concat_string_int_comparison2(bool async) + { + await base.Concat_string_int_comparison2(async); + + AssertSql( + """ +@i='10' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE @i::text || b."String" = '10Seattle' +"""); + } + + public override async Task Concat_string_int_comparison3(bool async) + { + await base.Concat_string_int_comparison3(async); + + AssertSql( + """ +@p='30' +@j='21' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE @p::text || b."String" || @j::text || 42::text = '30Seattle2142' +"""); + } + + public override async Task Concat_string_int_comparison4(bool async) + { + await base.Concat_string_int_comparison4(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text || b."String" = '8Seattle' +"""); + } + + public override async Task Concat_string_string_comparison(bool async) + { + await base.Concat_string_string_comparison(async); + + AssertSql( + """ +@i='A' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE @i || b."String" = 'ASeattle' +"""); + } + + public override async Task Concat_method_comparison(bool async) + { + await base.Concat_method_comparison(async); + + AssertSql( + """ +@i='A' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE @i || b."String" = 'ASeattle' +"""); + } + + public override async Task Concat_method_comparison_2(bool async) + { + await base.Concat_method_comparison_2(async); + + AssertSql( + """ +@i='A' +@j='B' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE @i || @j || b."String" = 'ABSeattle' +"""); + } + + public override async Task Concat_method_comparison_3(bool async) + { + await base.Concat_method_comparison_3(async); + + AssertSql( + """ +@i='A' +@j='B' +@k='C' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE @i || @j || @k || b."String" = 'ABCSeattle' +"""); + } + + #endregion Concatenation + + #region LINQ Operators + + public override async Task FirstOrDefault(bool async) + { + await base.FirstOrDefault(async); + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE substr(b."String", 1, 1) = 'S' +"""); + } + + public override async Task LastOrDefault(bool async) + { + await base.LastOrDefault(async); + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE substr(b."String", length(b."String"), 1) = 'e' +"""); + } + + #endregion LINQ Operators + + #region Like + + public override async Task Where_Like_and_comparison(bool async) + { + await base.Where_Like_and_comparison(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE 'S%' AND b."Int" = 8 +"""); + } + + public override async Task Where_Like_or_comparison(bool async) + { + await base.Where_Like_or_comparison(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" LIKE 'S%' OR b."Int" = 2147483647 +"""); + } + + public override async Task Like_with_non_string_column_using_ToString(bool async) + { + await base.Like_with_non_string_column_using_ToString(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text LIKE '%5%' +"""); + } + + public override async Task Like_with_non_string_column_using_double_cast(bool async) + { + await base.Like_with_non_string_column_using_double_cast(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."Int"::text LIKE '%5%' +"""); + } + + #endregion Like + + #region Regex + + public override async Task Regex_IsMatch(bool async) + { + await base.Regex_IsMatch(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."String" ~ '(?p)^S' +"""); + } + + public override async Task Regex_IsMatch_constant_input(bool async) + { + await base.Regex_IsMatch_constant_input(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE 'Seattle' ~ ('(?p)' || b."String") +"""); + } + + #endregion Regex + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateOnlyTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateOnlyTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..245083c2ce --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateOnlyTranslationsGaussDBTest.cs @@ -0,0 +1,238 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; + +public class DateOnlyTranslationsGaussDBTest : DateOnlyTranslationsTestBase +{ + public DateOnlyTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Year(bool async) + { + await base.Year(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('year', b."DateOnly")::int = 1990 +"""); + } + + public override async Task Month(bool async) + { + await base.Month(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('month', b."DateOnly")::int = 11 +"""); + } + + public override async Task Day(bool async) + { + await base.Day(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('day', b."DateOnly")::int = 10 +"""); + } + + public override async Task DayOfYear(bool async) + { + await base.DayOfYear(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('doy', b."DateOnly")::int = 314 +"""); + } + + public override async Task DayOfWeek(bool async) + { + await base.DayOfWeek(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(date_part('dow', b."DateOnly"))::int = 6 +"""); + } + + public override async Task DayNumber(bool async) + { + await base.DayNumber(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateOnly" - DATE '0001-01-01' = 726780 +"""); + } + + public override async Task AddYears(bool async) + { + await base.AddYears(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateOnly" + INTERVAL '3 years' AS date) = DATE '1993-11-10' +"""); + } + + public override async Task AddMonths(bool async) + { + await base.AddMonths(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateOnly" + INTERVAL '3 months' AS date) = DATE '1991-02-10' +"""); + } + + public override async Task AddDays(bool async) + { + await base.AddDays(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateOnly" + 3 = DATE '1990-11-13' +"""); + } + + public override async Task DayNumber_subtraction(bool async) + { + await base.DayNumber_subtraction(async); + + AssertSql( + """ +@DayNumber='726775' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE (b."DateOnly" - DATE '0001-01-01') - @DayNumber = 5 +"""); + } + + public override async Task FromDateTime(bool async) + { + await base.FromDateTime(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS date) = DATE '1998-05-04' +"""); + } + + public override async Task FromDateTime_compared_to_property(bool async) + { + await base.FromDateTime_compared_to_property(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS date) = b."DateOnly" +"""); + } + + public override async Task FromDateTime_compared_to_constant_and_parameter(bool async) + { + await base.FromDateTime_compared_to_constant_and_parameter(async); + + AssertSql( + """ +@dateOnly='10/11/0002' (DbType = Date) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS date) IN (@dateOnly, DATE '1998-05-04') +"""); + } + + public override async Task ToDateTime_property_with_constant_TimeOnly(bool async) + { + await base.ToDateTime_property_with_constant_TimeOnly(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateOnly" + TIME '21:05:19.9405' = TIMESTAMP '2020-01-01T21:05:19.9405' +"""); + } + + public override async Task ToDateTime_property_with_property_TimeOnly(bool async) + { + await base.ToDateTime_property_with_property_TimeOnly(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateOnly" + b."TimeOnly" = TIMESTAMP '2020-01-01T15:30:10' +"""); + } + + public override async Task ToDateTime_constant_DateTime_with_property_TimeOnly(bool async) + { + await base.ToDateTime_constant_DateTime_with_property_TimeOnly(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE DATE '1990-11-10' + b."TimeOnly" = TIMESTAMP '1990-11-10T15:30:10' +"""); + } + + public override async Task ToDateTime_with_complex_DateTime(bool async) + { + await base.ToDateTime_with_complex_DateTime(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateOnly" + INTERVAL '1 years' AS date) + b."TimeOnly" = TIMESTAMP '2021-01-01T15:30:10' +"""); + } + + public override async Task ToDateTime_with_complex_TimeOnly(bool async) + { + await base.ToDateTime_with_complex_TimeOnly(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateOnly" + b."TimeOnly" + INTERVAL '1 hours' = TIMESTAMP '2020-01-01T16:30:10' +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeOffsetTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeOffsetTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..1f5580663c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeOffsetTranslationsGaussDBTest.cs @@ -0,0 +1,243 @@ +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; + +public class DateTimeOffsetTranslationsGaussDBTest : DateTimeOffsetTranslationsTestBase +{ + public DateTimeOffsetTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // Not supported by design (DateTimeOffset with non-zero offset) + public override Task Now(bool async) + => Assert.ThrowsAsync(() => base.Now(async)); + + public override async Task UtcNow(bool async) + { + await base.UtcNow(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTimeOffset" <> now() +"""); + } + + // The test compares with new DateTimeOffset().Date, which GaussDB sends as -infinity, causing a discrepancy with the client behavior + // which uses 1/1/1:0:0:0 + public override Task Date(bool async) + => Assert.ThrowsAsync(() => base.Date(async)); + + public override async Task Year(bool async) + { + await base.Year(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('year', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 1998 +"""); + } + + public override async Task Month(bool async) + { + await base.Month(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('month', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 5 +"""); + } + + public override async Task DayOfYear(bool async) + { + await base.DayOfYear(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('doy', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 124 +"""); + } + + public override async Task Day(bool async) + { + await base.Day(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('day', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 4 +"""); + } + + public override async Task Hour(bool async) + { + await base.Hour(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('hour', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 15 +"""); + } + + public override async Task Minute(bool async) + { + await base.Minute(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('minute', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 30 +"""); + } + + public override async Task Second(bool async) + { + await base.Second(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('second', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 10 +"""); + } + + // SQL translation not implemented, too annoying + public override Task Millisecond(bool async) + => AssertTranslationFailed(() => base.Millisecond(async)); + + // TODO: #3406 + public override Task Microsecond(bool async) + => AssertTranslationFailed(() => base.Microsecond(async)); + + // TODO: #3406 + public override Task Nanosecond(bool async) + => AssertTranslationFailed(() => base.Nanosecond(async)); + + public override async Task TimeOfDay(bool async) + { + await base.TimeOfDay(async); + + AssertSql( + """ +SELECT CAST(b."DateTimeOffset" AT TIME ZONE 'UTC' AS time) +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddYears(bool async) + { + await base.AddYears(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" + INTERVAL '1 years' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddMonths(bool async) + { + await base.AddMonths(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" + INTERVAL '1 months' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddDays(bool async) + { + await base.AddDays(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" + INTERVAL '1 days' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddHours(bool async) + { + await base.AddHours(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" + INTERVAL '1 hours' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddMinutes(bool async) + { + await base.AddMinutes(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" + INTERVAL '1 mins' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddSeconds(bool async) + { + await base.AddSeconds(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" + INTERVAL '1 secs' +FROM "BasicTypesEntities" AS b +"""); + } + + public override async Task AddMilliseconds(bool async) + { + await base.AddMilliseconds(async); + + AssertSql( + """ +SELECT b."DateTimeOffset" +FROM "BasicTypesEntities" AS b +"""); + } + + public override Task ToUnixTimeMilliseconds(bool async) + => AssertTranslationFailed(() => base.ToUnixTimeMilliseconds(async)); + + public override Task ToUnixTimeSecond(bool async) + => AssertTranslationFailed(() => base.ToUnixTimeSecond(async)); + + public override async Task Milliseconds_parameter_and_constant(bool async) + { + await base.Milliseconds_parameter_and_constant(async); + + AssertSql( + """ +SELECT count(*)::int +FROM "BasicTypesEntities" AS b +WHERE b."DateTimeOffset" = TIMESTAMPTZ '1902-01-02T10:00:00.123456+01:30' +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..63f47d4567 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsGaussDBTest.cs @@ -0,0 +1,260 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; + +/// +/// Note that is mapped to PG timestamp with time zone, as is the provider default; +/// this causes issues with various tests. See also , which +/// explicitly maps to timestamp without time zone. +/// +public class DateTimeTranslationsGaussDBTest : DateTimeTranslationsTestBase +{ + public DateTimeTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Now(bool async) + { + await base.Now(async); + + AssertSql( + """ +@myDatetime='2015-04-10T00:00:00.0000000' + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE now()::timestamp <> @myDatetime +"""); + } + + public override async Task UtcNow(bool async) + { + // Overriding to set Kind=Utc for timestamptz + var myDatetime = DateTime.SpecifyKind(new DateTime(2015, 4, 10), DateTimeKind.Utc); + + await AssertQuery( + async, + ss => ss.Set().Where(c => DateTime.UtcNow != myDatetime)); + + AssertSql( + """ +@myDatetime='2015-04-10T00:00:00.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE now() <> @myDatetime +"""); + } + + // DateTime.Today returns a Local DateTime, which can't be compared with timestamptz + // (see TemporalTranslationsGaussDBTimestampWithoutTimeZoneTest for a working version of this test) + public override Task Today(bool async) + => Assert.ThrowsAsync(() => base.Today(async)); + + public override async Task Date(bool async) + { + // Overriding to set Kind=Utc for timestamptz + var myDatetime = DateTime.SpecifyKind(new DateTime(1998, 5, 4), DateTimeKind.Utc); + + await AssertQuery( + async, + ss => ss.Set().Where(o => o.DateTime.Date == myDatetime)); + + AssertSql( + """ +@myDatetime='1998-05-04T00:00:00.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_trunc('day', b."DateTime", 'UTC') = @myDatetime +"""); + } + + public override async Task AddYear(bool async) + { + await base.AddYear(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('year', (b."DateTime" + INTERVAL '1 years') AT TIME ZONE 'UTC')::int = 1999 +"""); + } + + public override async Task Year(bool async) + { + await base.Year(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('year', b."DateTime" AT TIME ZONE 'UTC')::int = 1998 +"""); + } + + public override async Task Month(bool async) + { + await base.Month(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('month', b."DateTime" AT TIME ZONE 'UTC')::int = 5 +"""); + } + + public override async Task DayOfYear(bool async) + { + await base.DayOfYear(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('doy', b."DateTime" AT TIME ZONE 'UTC')::int = 124 +"""); + } + + public override async Task Day(bool async) + { + await base.Day(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('day', b."DateTime" AT TIME ZONE 'UTC')::int = 4 +"""); + } + + public override async Task Hour(bool async) + { + await base.Hour(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('hour', b."DateTime" AT TIME ZONE 'UTC')::int = 15 +"""); + } + + public override async Task Minute(bool async) + { + await base.Minute(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('minute', b."DateTime" AT TIME ZONE 'UTC')::int = 30 +"""); + } + + public override async Task Second(bool async) + { + await base.Second(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('second', b."DateTime" AT TIME ZONE 'UTC')::int = 10 +"""); + } + + // SQL translation not implemented, too annoying + public override Task Millisecond(bool async) + => AssertTranslationFailed(() => base.Millisecond(async)); + + public override async Task TimeOfDay(bool async) + { + await base.TimeOfDay(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time) = TIME '00:00:00' +"""); + } + + public override async Task subtract_and_TotalDays(bool async) + { + // Overriding to set Kind=Utc for timestamptz + var date = DateTime.SpecifyKind(new DateTime(1997, 1, 1), DateTimeKind.Utc); + + await AssertQuery( + async, + ss => ss.Set().Where(o => (o.DateTime - date).TotalDays > 365)); + + AssertSql( + """ +@date='1997-01-01T00:00:00.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('epoch', b."DateTime" - @date) / 86400.0 > 365.0 +"""); + } + + // DateTime.Parse() returns either a Local or Unspecified DateTime, which can't be compared with timestamptz + // (see TemporalTranslationsGaussDBTimestampWithoutTimeZoneTest for a working version of this test) + public override Task Parse_with_constant(bool async) + => Assert.ThrowsAsync(() => base.Parse_with_constant(async)); + + // DateTime.Parse() returns either a Local or Unspecified DateTime, which can't be compared with timestamptz + // (see TemporalTranslationsGaussDBTimestampWithoutTimeZoneTest for a working version of this test) + public override Task Parse_with_parameter(bool async) + => Assert.ThrowsAsync(() => base.Parse_with_parameter(async)); + + public override async Task New_with_constant(bool async) + { + // Overriding to set Kind=Utc for timestamptz + await AssertQuery( + async, + ss => ss.Set().Where(o => o.DateTime == new DateTime(1998, 5, 4, 15, 30, 10, DateTimeKind.Utc))); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" = TIMESTAMPTZ '1998-05-04T15:30:10Z' +"""); + } + + public override async Task New_with_parameters(bool async) + { + // Overriding to set Kind=Utc for timestamptz + var year = 1998; + var month = 5; + var date = 4; + var hour = 15; + + await AssertQuery( + async, + ss => ss.Set().Where(o => o.DateTime == new DateTime(year, month, date, hour, 30, 10, DateTimeKind.Utc))); + + AssertSql( + """ +@p='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."DateTime" = @p +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsWithoutTimeZoneTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsWithoutTimeZoneTest.cs similarity index 82% rename from test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsWithoutTimeZoneTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsWithoutTimeZoneTest.cs index c01b2c5cfb..9a99d99c78 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsWithoutTimeZoneTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsWithoutTimeZoneTest.cs @@ -3,15 +3,15 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; /// -/// Same as , but the property is mapped to a PostgreSQL +/// Same as , but the property is mapped to a GaussDB /// timestamp without time zone, which corresponds to a with /// . /// public class DateTimeTranslationsWithoutTimeZoneTest - : DateTimeTranslationsTestBase + : DateTimeTranslationsTestBase { public DateTimeTranslationsWithoutTimeZoneTest( - BasicTypesQueryNpgsqlTimestampWithoutTimeZoneFixture fixture, + BasicTypesQueryGaussDBTimestampWithoutTimeZoneFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { @@ -19,9 +19,9 @@ public DateTimeTranslationsWithoutTimeZoneTest( Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - public override async Task Now() + public override async Task Now(bool async) { - await base.Now(); + await base.Now(async); AssertSql( """ @@ -33,12 +33,13 @@ WHERE now()::timestamp <> @myDatetime """); } - public override async Task UtcNow() + public override async Task UtcNow(bool async) { // Overriding to set Kind=Utc for timestamptz. This test generally doesn't make much sense here. var myDatetime = DateTime.SpecifyKind(new DateTime(2015, 4, 10), DateTimeKind.Utc); await AssertQuery( + async, ss => ss.Set().Where(c => DateTime.UtcNow != myDatetime)); AssertSql( @@ -51,9 +52,9 @@ WHERE now() <> @myDatetime """); } - public override async Task Today() + public override async Task Today(bool async) { - await base.Today(); + await base.Today(async); AssertSql( """ @@ -63,9 +64,9 @@ public override async Task Today() """); } - public override async Task Date() + public override async Task Date(bool async) { - await base.Date(); + await base.Date(async); AssertSql( """ @@ -77,9 +78,9 @@ WHERE date_trunc('day', b."DateTime") = @myDatetime """); } - public override async Task AddYear() + public override async Task AddYear(bool async) { - await base.AddYear(); + await base.AddYear(async); AssertSql( """ @@ -89,9 +90,9 @@ WHERE date_part('year', b."DateTime" + INTERVAL '1 years')::int = 1999 """); } - public override async Task Year() + public override async Task Year(bool async) { - await base.Year(); + await base.Year(async); AssertSql( """ @@ -101,9 +102,9 @@ WHERE date_part('year', b."DateTime")::int = 1998 """); } - public override async Task Month() + public override async Task Month(bool async) { - await base.Month(); + await base.Month(async); AssertSql( """ @@ -113,9 +114,9 @@ WHERE date_part('month', b."DateTime")::int = 5 """); } - public override async Task DayOfYear() + public override async Task DayOfYear(bool async) { - await base.DayOfYear(); + await base.DayOfYear(async); AssertSql( """ @@ -125,9 +126,9 @@ WHERE date_part('doy', b."DateTime")::int = 124 """); } - public override async Task Day() + public override async Task Day(bool async) { - await base.Day(); + await base.Day(async); AssertSql( """ @@ -137,9 +138,9 @@ WHERE date_part('day', b."DateTime")::int = 4 """); } - public override async Task Hour() + public override async Task Hour(bool async) { - await base.Hour(); + await base.Hour(async); AssertSql( """ @@ -149,9 +150,9 @@ WHERE date_part('hour', b."DateTime")::int = 15 """); } - public override async Task Minute() + public override async Task Minute(bool async) { - await base.Minute(); + await base.Minute(async); AssertSql( """ @@ -161,9 +162,9 @@ WHERE date_part('minute', b."DateTime")::int = 30 """); } - public override async Task Second() + public override async Task Second(bool async) { - await base.Second(); + await base.Second(async); AssertSql( """ @@ -174,12 +175,12 @@ WHERE date_part('second', b."DateTime")::int = 10 } // SQL translation not implemented, too annoying - public override Task Millisecond() - => AssertTranslationFailed(() => base.Millisecond()); + public override Task Millisecond(bool async) + => AssertTranslationFailed(() => base.Millisecond(async)); - public override async Task TimeOfDay() + public override async Task TimeOfDay(bool async) { - await base.TimeOfDay(); + await base.TimeOfDay(async); AssertSql( """ @@ -189,9 +190,9 @@ public override async Task TimeOfDay() """); } - public override async Task subtract_and_TotalDays() + public override async Task subtract_and_TotalDays(bool async) { - await base.subtract_and_TotalDays(); + await base.subtract_and_TotalDays(async); AssertSql( """ @@ -203,9 +204,9 @@ WHERE date_part('epoch', b."DateTime" - @date) / 86400.0 > 365.0 """); } - public override async Task Parse_with_constant() + public override async Task Parse_with_constant(bool async) { - await base.Parse_with_constant(); + await base.Parse_with_constant(async); AssertSql( """ @@ -215,9 +216,9 @@ public override async Task Parse_with_constant() """); } - public override async Task Parse_with_parameter() + public override async Task Parse_with_parameter(bool async) { - await base.Parse_with_parameter(); + await base.Parse_with_parameter(async); AssertSql( """ @@ -229,9 +230,9 @@ public override async Task Parse_with_parameter() """); } - public override async Task New_with_constant() + public override async Task New_with_constant(bool async) { - await base.New_with_constant(); + await base.New_with_constant(async); AssertSql( """ @@ -241,9 +242,9 @@ public override async Task New_with_constant() """); } - public override async Task New_with_parameters() + public override async Task New_with_parameters(bool async) { - await base.New_with_parameters(); + await base.New_with_parameters(async); AssertSql( """ @@ -262,7 +263,7 @@ public virtual void Check_all_tests_overridden() private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class BasicTypesQueryNpgsqlTimestampWithoutTimeZoneFixture : BasicTypesQueryNpgsqlFixture + public class BasicTypesQueryGaussDBTimestampWithoutTimeZoneFixture : BasicTypesQueryGaussDBFixture { private BasicTypesData? _expectedData; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..9b8e197a42 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsGaussDBTest.cs @@ -0,0 +1,208 @@ +using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; + +public class TimeOnlyTranslationsGaussDBTest : TimeOnlyTranslationsTestBase +{ + public TimeOnlyTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Hour(bool async) + { + await base.Hour(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('hour', b."TimeOnly")::int = 15 +"""); + } + + public override async Task Minute(bool async) + { + await base.Minute(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('minute', b."TimeOnly")::int = 30 +"""); + } + + public override async Task Second(bool async) + { + await base.Second(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE date_part('second', b."TimeOnly")::int = 10 +"""); + } + + // Translation not yet implemented + public override Task Millisecond(bool async) + => AssertTranslationFailed(() => base.Millisecond(async)); + + // Translation not yet implemented + public override Task Microsecond(bool async) + => AssertTranslationFailed(() => base.Millisecond(async)); + + // Probably not relevant for GaussDB, which supports microsecond precision only + public override Task Nanosecond(bool async) + => AssertTranslationFailed(() => base.Millisecond(async)); + + public override async Task AddHours(bool async) + { + await base.AddHours(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeOnly" + INTERVAL '3 hours' = TIME '18:30:10' +"""); + } + + public override async Task AddMinutes(bool async) + { + await base.AddMinutes(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeOnly" + INTERVAL '3 mins' = TIME '15:33:10' +"""); + } + + public override async Task Add_TimeSpan(bool async) + { + await base.Add_TimeSpan(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeOnly" + INTERVAL '03:00:00' = TIME '18:30:10' +"""); + } + + public override async Task IsBetween(bool async) + { + await base.IsBetween(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeOnly" >= TIME '14:00:00' AND b."TimeOnly" < TIME '16:00:00' +"""); + } + + public override async Task Subtract(bool async) + { + await base.Subtract(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeOnly" - TIME '03:00:00' = INTERVAL '12:30:10' +"""); + } + + public override async Task FromDateTime_compared_to_property(bool async) + { + await base.FromDateTime_compared_to_property(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time without time zone) = b."TimeOnly" +"""); + } + + public override async Task FromDateTime_compared_to_parameter(bool async) + { + await base.FromDateTime_compared_to_parameter(async); + + AssertSql( + """ +@time='15:30' (DbType = Time) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time without time zone) = @time +"""); + } + + public override async Task FromDateTime_compared_to_constant(bool async) + { + await base.FromDateTime_compared_to_constant(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time without time zone) = TIME '15:30:10' +"""); + } + + public override async Task FromTimeSpan_compared_to_property(bool async) + { + await base.FromTimeSpan_compared_to_property(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan"::time without time zone < b."TimeOnly" +"""); + } + + public override async Task FromTimeSpan_compared_to_parameter(bool async) + { + await base.FromTimeSpan_compared_to_parameter(async); + + AssertSql( + """ +@time='01:02' (DbType = Time) + +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE b."TimeSpan"::time without time zone = @time +"""); + } + + public override async Task Order_by_FromTimeSpan(bool async) + { + // TODO: Base implementation is non-deterministic, remove this override once that's fixed on the EF side. + await AssertQuery( + async, + ss => ss.Set().OrderBy(x => TimeOnly.FromTimeSpan(x.TimeSpan)).ThenBy(x => x.Id), + assertOrder: true); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +ORDER BY b."TimeSpan"::time without time zone NULLS FIRST, b."Id" NULLS FIRST +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsGaussDBTest.cs new file mode 100644 index 0000000000..3750faf219 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsGaussDBTest.cs @@ -0,0 +1,72 @@ +namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; + +public class TimeSpanTranslationsGaussDBTest : TimeSpanTranslationsTestBase +{ + public TimeSpanTranslationsGaussDBTest(BasicTypesQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Hours(bool async) + { + await base.Hours(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(date_part('hour', b."TimeSpan"))::int = 3 +"""); + } + + public override async Task Minutes(bool async) + { + await base.Minutes(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(date_part('minute', b."TimeSpan"))::int = 4 +"""); + } + + public override async Task Seconds(bool async) + { + await base.Seconds(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(date_part('second', b."TimeSpan"))::int = 5 +"""); + } + + public override async Task Milliseconds(bool async) + { + await base.Milliseconds(async); + + AssertSql( + """ +SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" +FROM "BasicTypesEntities" AS b +WHERE floor(date_part('millisecond', b."TimeSpan"))::int % 1000 = 678 +"""); + } + + public override Task Microseconds(bool async) + => AssertTranslationFailed(() => base.Microseconds(async)); + + public override Task Nanoseconds(bool async) + => AssertTranslationFailed(() => base.Nanoseconds(async)); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs index e67bf09db2..b08be633af 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs @@ -90,7 +90,7 @@ public async Task Cannot_insert_local_datetime_into_timestamptz() [MemberData(nameof(IsAsyncData))] public async Task Compare_timestamp_column_to_local_DateTime_literal(bool async) { - // Note that we're in the Europe/Berlin timezone (see NpgsqlTestStore below) + // Note that we're in the Europe/Berlin timezone (see GaussDBTestStore below) await AssertQuery( async, ss => ss.Set().Where(e => e.TimestampDateTime == new DateTime(1998, 4, 12, 15, 26, 38, DateTimeKind.Local))); @@ -425,7 +425,7 @@ await AssertQuery( [ConditionalFact] public async Task DateTimeOffset_LocalDateTime() { - // Note that we're in the Europe/Berlin timezone (see NpgsqlTestStore below) + // Note that we're in the Europe/Berlin timezone (see GaussDBTestStore below) // We can't use AssertQuery since the local (expected) evaluation is dependent on the machine's timezone, which is out of // our control. @@ -858,7 +858,7 @@ public void Range_parameter_contains_timestamp_with_no_time_zone_column() // This scenario requires that the provider correctly infer the range's type mapping from the subtype's using var ctx = CreateContext(); - var range = new NpgsqlRange(new DateTime(1998, 4, 12), new DateTime(1998, 4, 13)); + var range = new GaussDBRange(new DateTime(1998, 4, 12), new DateTime(1998, 4, 13)); var id = ctx.Entities.Single(e => range.Contains(e.TimestampDateTime)).Id; Assert.Equal(1, id); @@ -903,10 +903,10 @@ public class Entity public DateTimeOffset[]? TimestampDateTimeOffsetArray { get; set; } - public NpgsqlRange TimestamptzDateTimeRange { get; set; } + public GaussDBRange TimestamptzDateTimeRange { get; set; } [Column(TypeName = "tsrange")] - public NpgsqlRange TimestampDateTimeRange { get; set; } + public GaussDBRange TimestampDateTimeRange { get; set; } } public class TimestampQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory @@ -914,11 +914,11 @@ public class TimestampQueryFixture : SharedStoreFixtureBase "TimestampQueryTest"; - // Set the PostgreSQL TimeZone parameter to something local, to ensure that operations which take TimeZone into account + // Set the GaussDB 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. protected override ITestStoreFactory TestStoreFactory - => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin"); + => new GaussDBTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin"); public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; @@ -1011,11 +1011,11 @@ public static IReadOnlyList CreateEntities() new DateTime(2015, 1, 28, 10, 45, 12, 345, DateTimeKind.Unspecified) }; - var utcDateTimeRange1 = new NpgsqlRange(utcDateTimeArray1[0], utcDateTimeArray1[1]); - var localDateTimeRange1 = new NpgsqlRange(localDateTimeArray1[0], localDateTimeArray1[1]); + var utcDateTimeRange1 = new GaussDBRange(utcDateTimeArray1[0], utcDateTimeArray1[1]); + var localDateTimeRange1 = new GaussDBRange(localDateTimeArray1[0], localDateTimeArray1[1]); - var utcDateTimeRange2 = new NpgsqlRange(utcDateTimeArray2[0], utcDateTimeArray2[1]); - var localDateTimeRange2 = new NpgsqlRange(localDateTimeArray2[0], localDateTimeArray2[1]); + var utcDateTimeRange2 = new GaussDBRange(utcDateTimeArray2[0], utcDateTimeArray2[1]); + var localDateTimeRange2 = new GaussDBRange(localDateTimeArray2[0], localDateTimeArray2[1]); return new List { diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs similarity index 96% rename from test/EFCore.PG.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs rename to test/EFCore.GaussDB.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs index 3be6eed489..09cf28b5c5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs @@ -8,12 +8,12 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations; /// /// See: https://www.postgresql.org/docs/current/pgtrgm.html /// -public class TrigramsTranslationsTest : IClassFixture +public class TrigramsTranslationsTest : IClassFixture { - private TrigramsQueryNpgsqlFixture Fixture { get; } + private TrigramsQueryGaussDBFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public TrigramsTranslationsTest(TrigramsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public TrigramsTranslationsTest(TrigramsQueryGaussDBFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); @@ -216,13 +216,13 @@ public void PgUnknownBinary_operator_precedence() /// /// Represents a fixture suitable for testing trigrams operators. /// - public class TrigramsQueryNpgsqlFixture : SharedStoreFixtureBase + public class TrigramsQueryGaussDBFixture : SharedStoreFixtureBase { protected override string StoreName => "TrigramsQueryTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/UdfDbFunctionGaussDBTests.cs b/test/EFCore.GaussDB.FunctionalTests/Query/UdfDbFunctionGaussDBTests.cs new file mode 100644 index 0000000000..74d923f231 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/UdfDbFunctionGaussDBTests.cs @@ -0,0 +1,785 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class UdfDbFunctionGaussDBTests : UdfDbFunctionTestBase +{ + // ReSharper disable once UnusedParameter.Local + public UdfDbFunctionGaussDBTests(UdfGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region Static + + [Fact] + public override void Scalar_Function_Extension_Method_Static() + { + base.Scalar_Function_Extension_Method_Static(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE "IsDate"(c."FirstName") = FALSE +"""); + } + + [Fact] + public override void Scalar_Function_With_Translator_Translates_Static() + { + base.Scalar_Function_With_Translator_Translates_Static(); + + AssertSql( + """ +@customerId='3' + +SELECT length(c."LastName") +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_ClientEval_Method_As_Translateable_Method_Parameter_Static() + => base.Scalar_Function_ClientEval_Method_As_Translateable_Method_Parameter_Static(); + + [Fact] + public override void Scalar_Function_Constant_Parameter_Static() + { + base.Scalar_Function_Constant_Parameter_Static(); + + AssertSql( + """ +@customerId='1' + +SELECT "CustomerOrderCount"(@customerId) +FROM "Customers" AS c +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Correlated_Static() + { + base.Scalar_Function_Anonymous_Type_Select_Correlated_Static(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 1 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Not_Correlated_Static() + { + base.Scalar_Function_Anonymous_Type_Select_Not_Correlated_Static(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(1) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 1 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Parameter_Static() + { + base.Scalar_Function_Anonymous_Type_Select_Parameter_Static(); + + AssertSql( + """ +@customerId='1' + +SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Nested_Static() + { + base.Scalar_Function_Anonymous_Type_Select_Nested_Static(); + + AssertSql( + """ +@starCount='3' +@customerId='3' + +SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Where_Correlated_Static() + { + base.Scalar_Function_Where_Correlated_Static(); + + AssertSql( + """ +SELECT lower(c."Id"::text) +FROM "Customers" AS c +WHERE "IsTopCustomer"(c."Id") +"""); + } + + [Fact(Skip = "https://github.com/dotnet/efcore/issues/25980")] + public override void Scalar_Function_Where_Not_Correlated_Static() + { + base.Scalar_Function_Where_Not_Correlated_Static(); + + AssertSql( + """ +@__startDate_0='2000-04-01T00:00:00.0000000' (Nullable = true) (DbType = DateTime) + +SELECT c."Id" +FROM "Customers" AS c +WHERE "GetCustomerWithMostOrdersAfterDate"(@__startDate_0) = c."Id" +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Where_Parameter_Static() + { + base.Scalar_Function_Where_Parameter_Static(); + + AssertSql( + """ +@period='0' + +SELECT c."Id" +FROM "Customers" AS c +WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(@period)) +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Where_Nested_Static() + { + base.Scalar_Function_Where_Nested_Static(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(0)) +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Correlated_Static() + { + base.Scalar_Function_Let_Correlated_Static(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 2 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Not_Correlated_Static() + { + base.Scalar_Function_Let_Not_Correlated_Static(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(2) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 2 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Not_Parameter_Static() + { + base.Scalar_Function_Let_Not_Parameter_Static(); + + AssertSql( + """ +@customerId='2' + +SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Nested_Static() + { + base.Scalar_Function_Let_Nested_Static(); + + AssertSql( + """ +@starCount='3' +@customerId='1' + +SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Nested_Function_Unwind_Client_Eval_Select_Static() + { + base.Scalar_Nested_Function_Unwind_Client_Eval_Select_Static(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +ORDER BY c."Id" NULLS FIRST +"""); + } + + [Fact] + public override void Scalar_Nested_Function_BCL_UDF_Static() + { + base.Scalar_Nested_Function_BCL_UDF_Static(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +WHERE 3 = abs("CustomerOrderCount"(c."Id")) +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Nested_Function_UDF_BCL_Static() + { + base.Scalar_Nested_Function_UDF_BCL_Static(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +WHERE 3 = "CustomerOrderCount"(abs(c."Id")) +LIMIT 2 +"""); + } + + [Fact] + public override void Nullable_navigation_property_access_preserves_schema_for_sql_function() + { + base.Nullable_navigation_property_access_preserves_schema_for_sql_function(); + + AssertSql( + """ +SELECT dbo."IdentityString"(c."FirstName") +FROM "Orders" AS o +INNER JOIN "Customers" AS c ON o."CustomerId" = c."Id" +ORDER BY o."Id" NULLS FIRST +LIMIT 1 +"""); + } + + #endregion + + #region Instance + + [Fact] + public override void Scalar_Function_Non_Static() + { + base.Scalar_Function_Non_Static(); + + AssertSql( + """ +SELECT "StarValue"(4, c."Id") AS "Id", "DollarValue"(2, c."LastName") AS "LastName" +FROM "Customers" AS c +WHERE c."Id" = 1 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Extension_Method_Instance() + { + base.Scalar_Function_Extension_Method_Instance(); + + AssertSql( + """ +SELECT count(*)::int +FROM "Customers" AS c +WHERE "IsDate"(c."FirstName") = FALSE +"""); + } + + [Fact] + public override void Scalar_Function_With_Translator_Translates_Instance() + { + base.Scalar_Function_With_Translator_Translates_Instance(); + + AssertSql( + """ +@customerId='3' + +SELECT length(c."LastName") +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Constant_Parameter_Instance() + { + base.Scalar_Function_Constant_Parameter_Instance(); + + AssertSql( + """ +@customerId='1' + +SELECT "CustomerOrderCount"(@customerId) +FROM "Customers" AS c +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Correlated_Instance() + { + base.Scalar_Function_Anonymous_Type_Select_Correlated_Instance(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 1 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Not_Correlated_Instance() + { + base.Scalar_Function_Anonymous_Type_Select_Not_Correlated_Instance(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(1) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 1 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Parameter_Instance() + { + base.Scalar_Function_Anonymous_Type_Select_Parameter_Instance(); + + AssertSql( + """ +@customerId='1' + +SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Anonymous_Type_Select_Nested_Instance() + { + base.Scalar_Function_Anonymous_Type_Select_Nested_Instance(); + + AssertSql( + """ +@starCount='3' +@customerId='3' + +SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Where_Correlated_Instance() + { + base.Scalar_Function_Where_Correlated_Instance(); + + AssertSql( + """ +SELECT lower(c."Id"::text) +FROM "Customers" AS c +WHERE "IsTopCustomer"(c."Id") +"""); + } + + [Fact(Skip = "https://github.com/dotnet/efcore/issues/25980")] + public override void Scalar_Function_Where_Not_Correlated_Instance() + { + base.Scalar_Function_Where_Not_Correlated_Instance(); + + AssertSql( + """ +@__startDate_1='2000-04-01T00:00:00.0000000' (Nullable = true) (DbType = DateTime) + +SELECT c."Id" +FROM "Customers" AS c +WHERE "GetCustomerWithMostOrdersAfterDate"(@__startDate_1) = c."Id" +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Where_Parameter_Instance() + { + base.Scalar_Function_Where_Parameter_Instance(); + + AssertSql( + """ +@period='0' + +SELECT c."Id" +FROM "Customers" AS c +WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(@period)) +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Where_Nested_Instance() + { + base.Scalar_Function_Where_Nested_Instance(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(0)) +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Correlated_Instance() + { + base.Scalar_Function_Let_Correlated_Instance(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 2 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Not_Correlated_Instance() + { + base.Scalar_Function_Let_Not_Correlated_Instance(); + + AssertSql( + """ +SELECT c."LastName", "CustomerOrderCount"(2) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = 2 +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Not_Parameter_Instance() + { + base.Scalar_Function_Let_Not_Parameter_Instance(); + + AssertSql( + """ +@customerId='2' + +SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Function_Let_Nested_Instance() + { + base.Scalar_Function_Let_Nested_Instance(); + + AssertSql( + """ +@starCount='3' +@customerId='1' + +SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" +FROM "Customers" AS c +WHERE c."Id" = @customerId +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Nested_Function_Unwind_Client_Eval_Select_Instance() + { + base.Scalar_Nested_Function_Unwind_Client_Eval_Select_Instance(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +ORDER BY c."Id" NULLS FIRST +"""); + } + + [Fact] + public override void Scalar_Nested_Function_BCL_UDF_Instance() + { + base.Scalar_Nested_Function_BCL_UDF_Instance(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +WHERE 3 = abs("CustomerOrderCount"(c."Id")) +LIMIT 2 +"""); + } + + [Fact] + public override void Scalar_Nested_Function_UDF_BCL_Instance() + { + base.Scalar_Nested_Function_UDF_BCL_Instance(); + + AssertSql( + """ +SELECT c."Id" +FROM "Customers" AS c +WHERE 3 = "CustomerOrderCount"(abs(c."Id")) +LIMIT 2 +"""); + } + + #endregion + +#if RELEASE + [ConditionalFact(Skip = "https://github.com/dotnet/efcore/pull/30388")] + public override void Scalar_Function_with_nullable_value_return_type_throws() {} +#endif + + protected class GaussDBUDFSqlContext(DbContextOptions options) : UDFSqlContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // IsDate is a built-in SQL Server function, that in the base class is mapped as built-in, which means we + // don't get any quotes. We remap it as non-built-in by including a (null) schema. + var isDateMethodInfo = typeof(UDFSqlContext).GetMethod(nameof(IsDateStatic)); + modelBuilder.HasDbFunction(isDateMethodInfo) + .HasTranslation( + args => new SqlFunctionExpression( + schema: null, + "IsDate", + args, + nullable: true, + argumentsPropagateNullability: args.Select(_ => true).ToList(), + isDateMethodInfo.ReturnType, + typeMapping: null)); + + var isDateMethodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(IsDateInstance)); + modelBuilder.HasDbFunction(isDateMethodInfo2) + .HasTranslation( + args => new SqlFunctionExpression( + schema: null, + "IsDate", + args, + nullable: true, + argumentsPropagateNullability: args.Select(_ => true).ToList(), + isDateMethodInfo2.ReturnType, + typeMapping: null)); + + // Base class maps to len(), but in GaussDB it's called length() + var methodInfo = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthStatic)); + modelBuilder.HasDbFunction(methodInfo) + .HasTranslation( + args => new SqlFunctionExpression( + "length", + args, + nullable: true, + argumentsPropagateNullability: args.Select(_ => true).ToList(), + methodInfo.ReturnType, + typeMapping: null)); + + var methodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthInstance)); + modelBuilder.HasDbFunction(methodInfo2) + .HasTranslation( + args => new SqlFunctionExpression( + "length", + args, + nullable: true, + argumentsPropagateNullability: args.Select(_ => true).ToList(), + methodInfo2.ReturnType, + typeMapping: null)); + } + } + + public class UdfGaussDBFixture : UdfFixtureBase + { + protected override string StoreName { get; } = "UDFDbFunctionGaussDBTests"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override Type ContextType { get; } = typeof(GaussDBUDFSqlContext); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't + // supported. + modelBuilder.Entity().Property(o => o.OrderDate).HasColumnType("timestamp without time zone"); + + // The following should make us send 'timestamp without time zone' for these functions, but it doesn't: + // https://github.com/dotnet/efcore/issues/25980 + var typeMappingSource = context.GetService(); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(UDFSqlContext.GetCustomerWithMostOrdersAfterDateStatic))) + .HasParameter("startDate").Metadata.TypeMapping = typeMappingSource.GetMapping("timestamp without time zone"); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(UDFSqlContext.GetCustomerWithMostOrdersAfterDateInstance))) + .HasParameter("startDate").Metadata.TypeMapping = typeMappingSource.GetMapping("timestamp without time zone"); + } + + protected override async Task SeedAsync(DbContext context) + { + await base.SeedAsync(context); + + await context.Database.ExecuteSqlRawAsync( + """ +CREATE FUNCTION "CustomerOrderCount" ("customerId" INTEGER) RETURNS INTEGER +AS $$ SELECT COUNT("Id")::INTEGER FROM "Orders" WHERE "CustomerId" = $1 $$ +LANGUAGE SQL; + +CREATE FUNCTION "StarValue" ("starCount" INTEGER, value TEXT) RETURNS TEXT +AS $$ SELECT repeat('*', $1) || $2 $$ +LANGUAGE SQL; + +CREATE FUNCTION "StarValue" ("starCount" INTEGER, value INTEGER) RETURNS TEXT +AS $$ SELECT repeat('*', $1) || $2 $$ +LANGUAGE SQL; + +CREATE FUNCTION "DollarValue" ("starCount" INTEGER, value TEXT) RETURNS TEXT +AS $$ SELECT repeat('$', $1) || $2 $$ +LANGUAGE SQL; + +CREATE FUNCTION "GetReportingPeriodStartDate" (period INTEGER) RETURNS DATE +AS $$ SELECT DATE '1998-01-01' $$ +LANGUAGE SQL; + +CREATE FUNCTION "GetCustomerWithMostOrdersAfterDate" (searchDate TIMESTAMP) RETURNS INTEGER +AS $$ + SELECT "CustomerId" + FROM "Orders" + WHERE "OrderDate" > $1 + GROUP BY "CustomerId" + ORDER BY COUNT("Id") DESC + LIMIT 1 +$$ LANGUAGE SQL; + +CREATE FUNCTION "IsTopCustomer" ("customerId" INTEGER) RETURNS BOOL +AS $$ SELECT $1 = 1 $$ +LANGUAGE SQL; + +CREATE SCHEMA IF NOT EXISTS dbo; + +CREATE FUNCTION dbo."IdentityString" ("customerName" TEXT) RETURNS TEXT +AS $$ SELECT $1 $$ +LANGUAGE SQL; + +CREATE FUNCTION "GetCustomerOrderCountByYear"("customerId" INT) +RETURNS TABLE ("CustomerId" INT, "Count" INT, "Year" INT) +AS $$ + SELECT "CustomerId", COUNT("Id")::INT, EXTRACT(year FROM "OrderDate")::INT + FROM "Orders" + WHERE "CustomerId" = $1 + GROUP BY "CustomerId", EXTRACT(year FROM "OrderDate") + ORDER BY EXTRACT(year FROM "OrderDate") +$$ LANGUAGE SQL; + +CREATE FUNCTION "StringLength"("s" TEXT) RETURNS INT +AS $$ SELECT LENGTH($1) $$ +LANGUAGE SQL; + +CREATE FUNCTION "GetCustomerOrderCountByYearOnlyFrom2000"("customerId" INT, "onlyFrom2000" BOOL) +RETURNS TABLE ("CustomerId" INT, "Count" INT, "Year" INT) +AS $$ + SELECT $1, COUNT("Id")::INT, EXTRACT(year FROM "OrderDate")::INT + FROM "Orders" + WHERE "CustomerId" = 1 AND (NOT $2 OR $2 IS NULL OR ($2 AND EXTRACT(year FROM "OrderDate") = 2000)) + GROUP BY "CustomerId", EXTRACT(year FROM "OrderDate") + ORDER BY EXTRACT(year FROM "OrderDate") +$$ LANGUAGE SQL; + +CREATE FUNCTION "GetTopTwoSellingProducts"() +RETURNS TABLE ("ProductId" INT, "AmountSold" INT) +AS $$ + SELECT "ProductId", SUM("Quantity")::INT AS "totalSold" + FROM "LineItem" + GROUP BY "ProductId" + ORDER BY "totalSold" DESC + LIMIT 2 +$$ LANGUAGE SQL; + +CREATE FUNCTION "GetOrdersWithMultipleProducts"("customerId" INT) +RETURNS TABLE ("OrderId" INT, "CustomerId" INT, "OrderDate" TIMESTAMP) +AS $$ + SELECT o."Id", $1, "OrderDate" + FROM "Orders" AS o + JOIN "LineItem" li ON o."Id" = li."OrderId" + WHERE o."CustomerId" = $1 + GROUP BY o."Id", "OrderDate" + HAVING COUNT("ProductId") > 1 +$$ LANGUAGE SQL; + +CREATE FUNCTION "AddValues" (a INT, b INT) RETURNS INT +AS $$ SELECT $1 + $2 $$ LANGUAGE SQL; + +CREATE FUNCTION "IsDate"(s TEXT) RETURNS BOOLEAN +AS $$ +BEGIN + PERFORM s::DATE; + RETURN TRUE; +EXCEPTION WHEN OTHERS THEN + RETURN FALSE; +END; +$$ LANGUAGE PLPGSQL; +"""); + + await context.SaveChangesAsync(); + } + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Query/WarningsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Query/WarningsGaussDBTest.cs new file mode 100644 index 0000000000..ff08507ee1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Query/WarningsGaussDBTest.cs @@ -0,0 +1,3 @@ +namespace Microsoft.EntityFrameworkCore.Query; + +public class WarningsGaussDBTest(QueryNoClientEvalGaussDBFixture fixture) : WarningsTestBase(fixture); diff --git a/test/EFCore.GaussDB.FunctionalTests/QueryExpressionInterceptionGaussDBTestBase.cs b/test/EFCore.GaussDB.FunctionalTests/QueryExpressionInterceptionGaussDBTestBase.cs new file mode 100644 index 0000000000..4524d7e7ae --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/QueryExpressionInterceptionGaussDBTestBase.cs @@ -0,0 +1,55 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class QueryExpressionInterceptionGaussDBTestBase( + QueryExpressionInterceptionGaussDBTestBase.InterceptionGaussDBFixtureBase fixture) + : QueryExpressionInterceptionTestBase(fixture) +{ + public abstract class InterceptionGaussDBFixtureBase : InterceptionFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkGaussDB(), injectedInterceptors); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new GaussDBExecutionStrategy(d)); + return builder; + } + } + + public class QueryExpressionInterceptionGaussDBTest(QueryExpressionInterceptionGaussDBTest.InterceptionGaussDBFixture fixture) + : QueryExpressionInterceptionGaussDBTestBase(fixture), IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override string StoreName + => "QueryExpressionInterception"; + + protected override bool ShouldSubscribeToDiagnosticListener + => false; + } + } + + public class QueryExpressionInterceptionWithDiagnosticsGaussDBTest( + QueryExpressionInterceptionWithDiagnosticsGaussDBTest.InterceptionGaussDBFixture fixture) + : QueryExpressionInterceptionGaussDBTestBase(fixture), + IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override string StoreName + => "QueryExpressionInterceptionWithDiagnostics"; + + protected override bool ShouldSubscribeToDiagnosticListener + => true; + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/SaveChangesInterceptionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/SaveChangesInterceptionGaussDBTest.cs new file mode 100644 index 0000000000..821c49c498 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/SaveChangesInterceptionGaussDBTest.cs @@ -0,0 +1,68 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class SaveChangesInterceptionGaussDBTestBase(SaveChangesInterceptionGaussDBTestBase.InterceptionGaussDBFixtureBase fixture) + : SaveChangesInterceptionTestBase(fixture) +{ + public abstract class InterceptionGaussDBFixtureBase : InterceptionFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkGaussDB(), injectedInterceptors); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new GaussDBExecutionStrategy(d)); + return builder; + } + } + + public class SaveChangesInterceptionGaussDBTest(SaveChangesInterceptionGaussDBTest.InterceptionGaussDBFixture fixture) + : SaveChangesInterceptionGaussDBTestBase(fixture), IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override string StoreName + => "SaveChangesInterception"; + + protected override bool ShouldSubscribeToDiagnosticListener + => false; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new GaussDBExecutionStrategy(d)); + return builder; + } + } + } + + public class SaveChangesInterceptionWithDiagnosticsGaussDBTest( + SaveChangesInterceptionWithDiagnosticsGaussDBTest.InterceptionGaussDBFixture fixture) + : SaveChangesInterceptionGaussDBTestBase(fixture), + IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override string StoreName + => "SaveChangesInterceptionWithDiagnostics"; + + protected override bool ShouldSubscribeToDiagnosticListener + => true; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder(base.AddOptions(builder)) + .ExecutionStrategy(d => new GaussDBExecutionStrategy(d)); + return builder; + } + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Scaffolding/CompiledModelGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Scaffolding/CompiledModelGaussDBTest.cs new file mode 100644 index 0000000000..c9989d3a54 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Scaffolding/CompiledModelGaussDBTest.cs @@ -0,0 +1,59 @@ +// using System.Runtime.CompilerServices; +// using HuaweiCloud.EntityFrameworkCore.GaussDB.Design.Internal; +// using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +// using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; +// +// namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding; +// +// public class CompiledModelGaussDBTest : CompiledModelRelationalTestBase +// { +// protected override TestHelpers TestHelpers => GaussDBTestHelpers.Instance; +// protected override ITestStoreFactory TestStoreFactory => GaussDBTestStoreFactory.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 GaussDBDbContextOptionsBuilder(builder).UseNetTopologySuite(); +// return builder; +// } +// +// protected override void AddDesignTimeServices(IServiceCollection services) +// => new GaussDBNetTopologySuiteDesignTimeServices().ConfigureDesignTimeServices(services); +// +// protected override BuildSource AddReferences(BuildSource build, [CallerFilePath] string filePath = "") +// { +// base.AddReferences(build); +// build.References.Add(BuildReference.ByName("HuaweiCloud.EntityFrameworkCore.GaussDB")); +// build.References.Add(BuildReference.ByName("HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite")); +// build.References.Add(BuildReference.ByName("GaussDB")); +// build.References.Add(BuildReference.ByName("NetTopologySuite")); +// return build; +// } +// } diff --git a/test/EFCore.GaussDB.FunctionalTests/Scaffolding/GaussDBDatabaseModelFactoryTest.cs b/test/EFCore.GaussDB.FunctionalTests/Scaffolding/GaussDBDatabaseModelFactoryTest.cs new file mode 100644 index 0000000000..8474fb74e9 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Scaffolding/GaussDBDatabaseModelFactoryTest.cs @@ -0,0 +1,2229 @@ +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +// ReSharper disable InconsistentNaming +// ReSharper disable StringLiteralTypo +namespace Microsoft.EntityFrameworkCore.Scaffolding; + +public class GaussDBDatabaseModelFactoryTest : IClassFixture +{ + // ReSharper disable once MemberCanBePrivate.Global + protected GaussDBDatabaseModelFixture Fixture { get; } + + public GaussDBDatabaseModelFactoryTest(GaussDBDatabaseModelFixture fixture) + { + Fixture = fixture; + Fixture.ListLoggerFactory.Clear(); + } + + #region Sequences + + [Fact] + public void Create_sequences_with_facets() + { + var supportsDataType = TestEnvironment.PostgresVersion >= new Version(10, 0); + + Test( + $""" +CREATE SEQUENCE "DefaultFacetsSequence"; + +CREATE SEQUENCE db2."CustomFacetsSequence" + {(supportsDataType ? "AS int" : null)} + START WITH 1 + INCREMENT BY 2 + MAXVALUE 8 + MINVALUE -3 + CYCLE; +""", + [], + [], + dbModel => + { + var defaultSequence = dbModel.Sequences.First(ds => ds.Name == "DefaultFacetsSequence"); + Assert.Equal("public", defaultSequence.Schema); + Assert.Equal("DefaultFacetsSequence", defaultSequence.Name); + Assert.Equal("bigint", defaultSequence.StoreType); + Assert.Null(defaultSequence.IsCyclic); + Assert.Null(defaultSequence.IncrementBy); + Assert.Null(defaultSequence.StartValue); + Assert.Null(defaultSequence.MinValue); + Assert.Null(defaultSequence.MaxValue); + + var customSequence = dbModel.Sequences.First(ds => ds.Name == "CustomFacetsSequence"); + Assert.Equal("db2", customSequence.Schema); + Assert.Equal("CustomFacetsSequence", customSequence.Name); + Assert.Equal(supportsDataType ? "integer" : "bigint", customSequence.StoreType); + Assert.True(customSequence.IsCyclic); + Assert.Equal(2, customSequence.IncrementBy); + Assert.Equal(1, customSequence.StartValue); + Assert.Equal(-3, customSequence.MinValue); + Assert.Equal(8, customSequence.MaxValue); + }, + """ +DROP SEQUENCE "DefaultFacetsSequence"; +DROP SEQUENCE db2."CustomFacetsSequence" +"""); + } + + [ConditionalFact] + [MinimumPostgresVersion(11, 0)] + public void Sequence_min_max_start_values_are_null_if_default() + => Test( + """ +CREATE SEQUENCE "SmallIntSequence" AS smallint; +CREATE SEQUENCE "IntSequence" AS int; +CREATE SEQUENCE "BigIntSequence" AS bigint; +""", + [], + [], + dbModel => + { + Assert.All( + dbModel.Sequences, + s => + { + Assert.Null(s.StartValue); + Assert.Null(s.MinValue); + Assert.Null(s.MaxValue); + }); + }, + """ +DROP SEQUENCE "SmallIntSequence"; +DROP SEQUENCE "IntSequence"; +DROP SEQUENCE "BigIntSequence"; +"""); + + [Fact] + public void Filter_sequences_based_on_schema() + => Test( + """ +CREATE SEQUENCE "Sequence"; +CREATE SEQUENCE db2."Sequence" +""", + [], + ["db2"], + dbModel => + { + var sequence = Assert.Single(dbModel.Sequences); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("db2", sequence.Schema); + Assert.Equal("Sequence", sequence.Name); + Assert.Equal("bigint", sequence.StoreType); + }, + """ +DROP SEQUENCE "Sequence"; +DROP SEQUENCE db2."Sequence"; +"""); + + #endregion + + #region Model + + [Fact] + public void Set_default_schema() + => Test( + "SELECT 1", + [], + [], + dbModel => + { + Assert.Equal("public", dbModel.DefaultSchema); + }, + null); + + [Fact] + public void Create_tables() + => Test( + """ +CREATE TABLE "Everest" (id int); +CREATE TABLE "Denali" (id int); +""", + [], + [], + dbModel => + { + Assert.Collection( + dbModel.Tables.OrderBy(t => t.Name), + d => + { + Assert.Equal("public", d.Schema); + Assert.Equal("Denali", d.Name); + }, + e => + { + Assert.Equal("public", e.Schema); + Assert.Equal("Everest", e.Name); + }); + }, + """ +DROP TABLE "Everest"; +DROP TABLE "Denali"; +"""); + + #endregion + + #region FilteringSchemaTable + + [Fact] + public void Filter_schemas() + => Test( + """ +CREATE TABLE db2."K2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); +""", + [], + ["db2"], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "Kilimanjaro"; +DROP TABLE db2."K2"; +"""); + + [Fact] + public void Filter_tables() + => Test( + """ +CREATE TABLE "K2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B), FOREIGN KEY (B) REFERENCES "K2" (A)); +""", + ["K2"], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "Kilimanjaro"; +DROP TABLE "K2"; +"""); + + [Fact] + public void Filter_tables_with_qualified_name() + => Test( + """ +CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); +""", + [""" + "K.2" + """], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K.2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "Kilimanjaro"; +DROP TABLE "K.2"; +"""); + + [Fact] + public void Filter_tables_with_schema_qualified_name1() + => Test( + """ +CREATE TABLE public."K2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE db2."K2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); +""", + ["public.K2"], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "Kilimanjaro"; +DROP TABLE "K2"; +DROP TABLE db2."K2"; +"""); + + [Fact] + public void Filter_tables_with_schema_qualified_name2() + => Test( + """ +CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "db.2"."K.2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "db.2"."Kilimanjaro" (Id int, B varchar, UNIQUE (B)); +""", + [""" + "db.2"."K.2" + """], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K.2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "db.2"."Kilimanjaro"; +DROP TABLE "K.2"; +DROP TABLE "db.2"."K.2"; +"""); + + [Fact] + public void Filter_tables_with_schema_qualified_name3() + => Test( + """ +CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "db2"."K.2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); +""", + [""" + public."K.2" + """], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K.2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "Kilimanjaro"; +DROP TABLE "K.2"; +DROP TABLE db2."K.2"; +"""); + + [Fact] + public void Filter_tables_with_schema_qualified_name4() + => Test( + """ +CREATE TABLE "K2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "db.2"."K2" (Id int, A varchar, UNIQUE (A)); +CREATE TABLE "db.2"."Kilimanjaro" (Id int, B varchar, UNIQUE (B)); +""", + [""" + "db.2".K2 + """], + [], + dbModel => + { + var table = Assert.Single(dbModel.Tables); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("K2", table.Name); + Assert.Equal(2, table.Columns.Count); + Assert.Single(table.UniqueConstraints); + Assert.Empty(table.ForeignKeys); + }, + """ +DROP TABLE "db.2"."Kilimanjaro"; +DROP TABLE "K2"; +DROP TABLE "db.2"."K2"; +"""); + + [Fact] + public void Complex_filtering_validation() + => Test( + """ +CREATE SEQUENCE public."Sequence"; +CREATE SEQUENCE "db2"."Sequence"; + +CREATE TABLE "db.2"."QuotedTableName" ("Id" int PRIMARY KEY); +CREATE TABLE "db.2"."Table.With.Dot" ("Id" int PRIMARY KEY); +CREATE TABLE "db.2"."SimpleTableName" ("Id" int PRIMARY KEY); +CREATE TABLE "db.2"."JustTableName" ("Id" int PRIMARY KEY); + +CREATE TABLE public."QuotedTableName" ("Id" int PRIMARY KEY); +CREATE TABLE public."Table.With.Dot" ("Id" int PRIMARY KEY); +CREATE TABLE public."SimpleTableName" ("Id" int PRIMARY KEY); +CREATE TABLE public."JustTableName" ("Id" int PRIMARY KEY); + +CREATE TABLE db2."QuotedTableName" ("Id" int PRIMARY KEY); +CREATE TABLE db2."Table.With.Dot" ("Id" int PRIMARY KEY); +CREATE TABLE db2."SimpleTableName" ("Id" int PRIMARY KEY); +CREATE TABLE db2."JustTableName" ("Id" int PRIMARY KEY); + +CREATE TABLE "db2"."PrincipalTable" ( + "Id" int PRIMARY KEY, + "UC1" text, + "UC2" int, + "Index1" bit, + "Index2" bigint, + CONSTRAINT "UX" UNIQUE ("UC1", "UC2") +); + +CREATE INDEX "IX_COMPOSITE" ON "db2"."PrincipalTable" ("Index2", "Index1"); + +CREATE TABLE "db2"."DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeyId1" text, + "ForeignKeyId2" int, + FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "db2"."PrincipalTable"("UC1", "UC2") ON DELETE CASCADE +); +""", + [ + """ + "db.2"."QuotedTableName" + """, + """ + "db.2".SimpleTableName + """, + """ + public."Table.With.Dot" + """, + """ + public."SimpleTableName" + """, + """ + "JustTableName" + """ + ], + ["db2"], + dbModel => + { + var sequence = Assert.Single(dbModel.Sequences); + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("db2", sequence.Schema); + + Assert.Single(dbModel.Tables, t => t is { Schema: "db.2", Name: "QuotedTableName" }); + Assert.DoesNotContain(dbModel.Tables, t => t is { Schema: "db.2", Name: "Table.With.Dot" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "db.2", Name: "SimpleTableName" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "db.2", Name: "JustTableName" }); + + Assert.DoesNotContain(dbModel.Tables, t => t is { Schema: "public", Name: "QuotedTableName" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "public", Name: "Table.With.Dot" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "public", Name: "SimpleTableName" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "public", Name: "JustTableName" }); + + Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "QuotedTableName" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "Table.With.Dot" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "SimpleTableName" }); + Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "JustTableName" }); + + var principalTable = Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "PrincipalTable" }); + // ReSharper disable once PossibleNullReferenceException + Assert.NotNull(principalTable.PrimaryKey); + Assert.Single(principalTable.UniqueConstraints); + Assert.Single(principalTable.Indexes); + + var dependentTable = Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "DependentTable" }); + // ReSharper disable once PossibleNullReferenceException + Assert.Single(dependentTable.ForeignKeys); + }, + """ +DROP SEQUENCE public."Sequence"; +DROP SEQUENCE db2."Sequence"; + +DROP TABLE "db.2"."QuotedTableName"; +DROP TABLE "db.2"."Table.With.Dot"; +DROP TABLE "db.2"."SimpleTableName"; +DROP TABLE "db.2"."JustTableName"; + +DROP TABLE public."QuotedTableName"; +DROP TABLE public."Table.With.Dot"; +DROP TABLE public."SimpleTableName"; +DROP TABLE public."JustTableName"; + +DROP TABLE db2."QuotedTableName"; +DROP TABLE db2."Table.With.Dot"; +DROP TABLE db2."SimpleTableName"; +DROP TABLE db2."JustTableName"; +DROP TABLE db2."DependentTable"; +DROP TABLE db2."PrincipalTable"; +"""); + + #endregion + + #region Table + + [Fact] + public void Create_columns() + => Test( + """ +CREATE TABLE "Blogs" ( + "Id" int, + "Name" text NOT NULL +); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + Assert.Equal(2, table.Columns.Count); + Assert.All( + table.Columns, c => + { + Assert.Equal("public", c.Table.Schema); + Assert.Equal("Blogs", c.Table.Name); + }); + + Assert.Single(table.Columns, c => c.Name == "Id"); + Assert.Single(table.Columns, c => c.Name == "Name"); + }, + """ + DROP TABLE "Blogs" + """); + + [Fact] + public void Create_view_columns() + => Test( + """ +CREATE VIEW "BlogsView" AS SELECT 100::int AS "Id", ''::text AS "Name"; +""", + [], + [], + dbModel => + { + var table = Assert.IsType(dbModel.Tables.Single()); + + Assert.Equal(2, table.Columns.Count); + Assert.Null(table.PrimaryKey); + Assert.All( + table.Columns, c => + { + Assert.Equal("public", c.Table.Schema); + Assert.Equal("BlogsView", c.Table.Name); + }); + + Assert.Single(table.Columns, c => c.Name == "Id"); + Assert.Single(table.Columns, c => c.Name == "Name"); + }, + """DROP VIEW "BlogsView";"""); + + [Fact] + public void Create_materialized_view_columns() + => Test( + """ +CREATE MATERIALIZED VIEW "BlogsView" AS SELECT 100::int AS "Id", ''::text AS "Name"; +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + Assert.Equal(2, table.Columns.Count); + Assert.Null(table.PrimaryKey); + Assert.All( + table.Columns, c => + { + Assert.Equal("public", c.Table.Schema); + Assert.Equal("BlogsView", c.Table.Name); + }); + + Assert.Single(table.Columns, c => c.Name == "Id"); + Assert.Single(table.Columns, c => c.Name == "Name"); + }, + """DROP MATERIALIZED VIEW "BlogsView";"""); + + [Fact] + public void Create_primary_key() + => Test( + """ +CREATE TABLE "PrimaryKeyTable" ("Id" int PRIMARY KEY); +""", + [], + [], + dbModel => + { + var pk = dbModel.Tables.Single().PrimaryKey!; + + Assert.Equal("public", pk.Table!.Schema); + Assert.Equal("PrimaryKeyTable", pk.Table.Name); + Assert.StartsWith("PrimaryKeyTable_pkey", pk.Name); + Assert.Equal(["Id"], pk.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "PrimaryKeyTable" + """); + + [Fact] + public void Create_unique_constraints() + => Test( + """ +CREATE TABLE "UniqueConstraint" ( + "Id" int, + "Name" int Unique, + "IndexProperty" int, + "Unq1" int, + "Unq2" int, + UNIQUE ("Unq1", "Unq2") +); + +CREATE INDEX "IX_INDEX" on "UniqueConstraint" ("IndexProperty"); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + Assert.Equal(2, table.UniqueConstraints.Count); + + var firstConstraint = table.UniqueConstraints.Single(c => c.Columns.Count == 1); + Assert.Equal("public", firstConstraint.Table.Schema); + Assert.Equal("UniqueConstraint", firstConstraint.Table.Name); + //Assert.StartsWith("UQ__UniqueCo", uniqueConstraint.Name); + 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(["Unq1", "Unq2"], secondConstraint.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "UniqueConstraint" + """); + + [Fact] + public void Create_indexes() + => Test( + """ +CREATE TABLE "IndexTable" ( + "Id" int, + "Name" int, + "IndexProperty" int, + "ConstraintProperty" int, + UNIQUE ("ConstraintProperty") +); + +CREATE INDEX "IX_NAME" on "IndexTable" ("Name"); +CREATE INDEX "IX_INDEX" on "IndexTable" ("IndexProperty"); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + // Unique constraints should *not* be modelled as indices + Assert.Equal(2, table.Indexes.Count); + Assert.All( + table.Indexes, c => + { + Assert.Equal("public", c.Table!.Schema); + Assert.Equal("IndexTable", c.Table.Name); + }); + + Assert.Single(table.Indexes, c => c.Name == "IX_NAME"); + Assert.Single(table.Indexes, c => c.Name == "IX_INDEX"); + }, + """ + DROP TABLE "IndexTable" + """); + + [Fact] + public void Create_foreign_keys() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id" int PRIMARY KEY +); + +CREATE TABLE "FirstDependent" ( + "Id" int PRIMARY KEY, + "ForeignKeyId" int, + FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE +); + +CREATE TABLE "SecondDependent" ( + "Id" int PRIMARY KEY, + FOREIGN KEY ("Id") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION +); +""", + [], + [], + dbModel => + { + var firstFk = Assert.Single(dbModel.Tables.Single(t => t.Name == "FirstDependent").ForeignKeys); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", firstFk.Table.Schema); + Assert.Equal("FirstDependent", firstFk.Table.Name); + Assert.Equal("public", firstFk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", firstFk.PrincipalTable.Name); + 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); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", secondFk.Table.Schema); + Assert.Equal("SecondDependent", secondFk.Table.Name); + Assert.Equal("public", secondFk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", secondFk.PrincipalTable.Name); + 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); + }, + """ +DROP TABLE "SecondDependent"; +DROP TABLE "FirstDependent"; +DROP TABLE "PrincipalTable"; +"""); + + #endregion + + #region ColumnFacets + + [Fact] + public void Column_with_domain_assigns_underlying_store_type() + { + Fixture.TestStore.ExecuteNonQuery( + """ +CREATE DOMAIN public.text_domain AS text; +CREATE DOMAIN db2.text_domain AS int; +CREATE DOMAIN public.char_domain AS char(3); +"""); + + Test( + """ +CREATE TABLE domains ( + id int, + text_domain public.text_domain NULL, + char_domain public.char_domain NULL +) +""", + [], + [], + dbModel => + { + var textDomainColumn = Assert.Single(dbModel.Tables.Single().Columns, c => c.Name == "text_domain"); + Assert.Equal("text", textDomainColumn?.StoreType); + + var charDomainColumn = Assert.Single(dbModel.Tables.Single().Columns, c => c.Name == "char_domain"); + Assert.Equal("character(3)", charDomainColumn?.StoreType); + + var nonDomainColumn = Assert.Single(dbModel.Tables.Single().Columns, c => c.Name == "id"); + Assert.Equal("integer", nonDomainColumn?.StoreType); + }, + """ +DROP TABLE domains; +DROP DOMAIN public.text_domain; +DROP DOMAIN public.char_domain; +DROP DOMAIN db2.text_domain; +"""); + } + + // Note: in GaussDB decimal is simply an alias for numeric + [Fact] + public void Decimal_numeric_types_have_precision_scale() + => Test( + """ +CREATE TABLE "NumericColumns" ( + "Id" int, + "numericColumn" numeric NOT NULL, + "numeric152Column" numeric(15, 2) NOT NULL, + "numeric18Column" numeric(18) NOT NULL +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Equal("numeric", columns.Single(c => c.Name == "numericColumn").StoreType); + 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" + """); + + [Fact] + public void Specific_max_length_are_add_to_store_type() + => Test( + """ +CREATE TABLE "LengthColumns" ( + "Id" int, + "char10Column" char(10) NULL, + "varchar66Column" varchar(66) NULL, + "bit111Column" bit(111) NULL, + "varbit123Column" varbit(123) NULL, + "varchar66ArrayColumn" varchar(66)[] NULL, + "varbit123ArrayColumn" varbit(123)[] NULL +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Equal("character(10)", columns.Single(c => c.Name == "char10Column").StoreType); + Assert.Equal("character varying(66)", columns.Single(c => c.Name == "varchar66Column").StoreType); + Assert.Equal("bit(111)", columns.Single(c => c.Name == "bit111Column").StoreType); + Assert.Equal("bit varying(123)", columns.Single(c => c.Name == "varbit123Column").StoreType); + 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" + """); + + [Fact] + public void Datetime_types_have_precision_if_non_null_scale() + => Test( + """ +CREATE TABLE "LengthColumns" ( + "Id" int, + "time1Column" time(1) NULL, + "timetz2Column" timetz(2) NULL, + "timestamp3Column" timestamp(3) NULL, + "timestamptz4Column" timestamptz(4) NULL, + "interval5Column" interval(5) NULL +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Equal("time(1) without time zone", columns.Single(c => c.Name == "time1Column").StoreType); + Assert.Equal("time(2) with time zone", columns.Single(c => c.Name == "timetz2Column").StoreType); + Assert.Equal("timestamp(3) without time zone", columns.Single(c => c.Name == "timestamp3Column").StoreType); + 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" + """); + + [Fact] + public void Store_types_without_any_facets() + => Test( + """ +CREATE TABLE "NoFacetTypes" ( + "Id" int, + "boolColumn" bool, + "byteaColumn" bytea, + "floatColumn" float4, + "doubleColumn" float8, + "decimalColumn" decimal, + "moneyColumn" money, + "guidColumn" uuid, + "shortColumn" int2, + "intColumn" int4, + "longColumn" int8, + "textColumn" text, + "jsonbColumn" jsonb, + "jsonColumn" json, + "timestampColumn" timestamp, + /* TODO: timestamptz */ + "intervalColumn" interval, + "timetzColumn" timetz, + "macaddrColumn" macaddr, + "inetColumn" inet, + "pointColumn" point, + "lineColumn" line, + "xidColumn" xid, + "textArrayColumn" text[] +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single(t => t.Name == "NoFacetTypes").Columns; + + Assert.Equal("boolean", columns.Single(c => c.Name == "boolColumn").StoreType); + Assert.Equal("bytea", columns.Single(c => c.Name == "byteaColumn").StoreType); + Assert.Equal("real", columns.Single(c => c.Name == "floatColumn").StoreType); + Assert.Equal("double precision", columns.Single(c => c.Name == "doubleColumn").StoreType); + Assert.Equal("numeric", columns.Single(c => c.Name == "decimalColumn").StoreType); + Assert.Equal("money", columns.Single(c => c.Name == "moneyColumn").StoreType); + Assert.Equal("uuid", columns.Single(c => c.Name == "guidColumn").StoreType); + Assert.Equal("smallint", columns.Single(c => c.Name == "shortColumn").StoreType); + Assert.Equal("integer", columns.Single(c => c.Name == "intColumn").StoreType); + Assert.Equal("bigint", columns.Single(c => c.Name == "longColumn").StoreType); + Assert.Equal("text", columns.Single(c => c.Name == "textColumn").StoreType); + Assert.Equal("jsonb", columns.Single(c => c.Name == "jsonbColumn").StoreType); + Assert.Equal("json", columns.Single(c => c.Name == "jsonColumn").StoreType); + Assert.Equal("timestamp without time zone", columns.Single(c => c.Name == "timestampColumn").StoreType); + Assert.Equal("interval", columns.Single(c => c.Name == "intervalColumn").StoreType); + Assert.Equal("time with time zone", columns.Single(c => c.Name == "timetzColumn").StoreType); + Assert.Equal("macaddr", columns.Single(c => c.Name == "macaddrColumn").StoreType); + Assert.Equal("inet", columns.Single(c => c.Name == "inetColumn").StoreType); + Assert.Equal("point", columns.Single(c => c.Name == "pointColumn").StoreType); + Assert.Equal("line", columns.Single(c => c.Name == "lineColumn").StoreType); + Assert.Equal("xid", columns.Single(c => c.Name == "xidColumn").StoreType); + Assert.Equal("text[]", columns.Single(c => c.Name == "textArrayColumn").StoreType); + }, + """ + DROP TABLE "NoFacetTypes" + """); + + [Fact] + public void Default_values_are_stored() + => Test( + """ +CREATE TABLE "DefaultValues" ( + "Id" int, + "FixedDefaultValue" timestamp NOT NULL DEFAULT ('1999-01-08') +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + Assert.Equal( + "'1999-01-08 00:00:00'::timestamp without time zone", + columns.Single(c => c.Name == "FixedDefaultValue").DefaultValueSql); + }, + """ + DROP TABLE "DefaultValues" + """); + + [ConditionalFact] + [MinimumPostgresVersion(12, 0)] + public void Computed_values_are_stored() + => Test( + """ +CREATE TABLE "ComputedValues" ( + "Id" int, + "A" int NOT NULL, + "B" int NOT NULL, + "SumOfAAndB" int GENERATED ALWAYS AS ("A" + "B") STORED +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + // Note that on-the-fly computed columns aren't (yet) supported by GaussDB, only stored/persisted + // columns. + var column = columns.Single(c => c.Name == "SumOfAAndB"); + Assert.Null(column.DefaultValueSql); + Assert.Equal("""("A" + "B")""", column.ComputedColumnSql); + Assert.True(column.IsStored); + }, + """ + DROP TABLE "ComputedValues" + """); + + [Fact] + public void ValueGenerated_is_set_for_default_and_serial_column() + => Test( + """ +CREATE TABLE "ValueGeneratedProperties" ( + "Id" SERIAL, + "NoValueGenerationColumn" text, + "FixedDefaultValue" timestamp NOT NULL DEFAULT ('1999-01-08') +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Equal(ValueGenerated.OnAdd, columns.Single(c => c.Name == "Id").ValueGenerated); + Assert.Null(columns.Single(c => c.Name == "NoValueGenerationColumn").ValueGenerated); + Assert.Null(columns.Single(c => c.Name == "FixedDefaultValue").ValueGenerated); + }, + """ + DROP TABLE "ValueGeneratedProperties" + """); + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void ValueGenerated_is_set_for_identity_column() + => Test( + """ +CREATE TABLE "ValueGeneratedProperties" ( + "Id1" INT GENERATED ALWAYS AS IDENTITY, + "Id2" INT GENERATED BY DEFAULT AS IDENTITY +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + 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" + """); + + [ConditionalFact] + [MinimumPostgresVersion(12, 0)] + public void ValueGenerated_is_set_for_computed_column() + => Test( + """ +CREATE TABLE "ValueGeneratedProperties" ( + "Id" INT GENERATED ALWAYS AS IDENTITY, + "A" int NOT NULL, + "B" int NOT NULL, + "SumOfAAndB" int GENERATED ALWAYS AS ("A" + "B") STORED +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Null(columns.Single(c => c.Name == "SumOfAAndB").ValueGenerated); + }, + """ + DROP TABLE "ValueGeneratedProperties" + """); + + [Fact] + public void Column_nullability_is_set() + => Test( + """ +CREATE TABLE "NullableColumns" ( + "Id" int, + "NullableInt" int NULL, + "NonNullableInt" int NOT NULL +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.True(columns.Single(c => c.Name == "NullableInt").IsNullable); + Assert.False(columns.Single(c => c.Name == "NonNullableInt").IsNullable); + }, + """ + DROP TABLE "NullableColumns" + """); + + [Fact] + public void Column_nullability_is_set_with_domain() + => Test( + """ +CREATE DOMAIN non_nullable_int AS int NOT NULL; + +CREATE TABLE "NullableColumnsDomain" ( + "Id" int, + "NullableInt" non_nullable_int NULL, + "NonNullString" non_nullable_int NOT NULL +) +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.False(columns.Single(c => c.Name == "NullableInt").IsNullable); + Assert.False(columns.Single(c => c.Name == "NonNullString").IsNullable); + }, + """ +DROP TABLE "NullableColumnsDomain"; +DROP DOMAIN non_nullable_int; +"""); + + [Fact] + public void System_columns_are_not_created() + => Test( + """ +CREATE TABLE "SystemColumnsTable" +( + "Id" int NOT NULL PRIMARY KEY +) +""", + [], + [], + dbModel => Assert.Single(dbModel.Tables.Single().Columns), + """ + DROP TABLE "SystemColumnsTable" + """); + + #endregion + + #region PrimaryKeyFacets + + [Fact] + public void Create_composite_primary_key() + => Test( + """ +CREATE TABLE "CompositePrimaryKeyTable" ( + "Id1" int, + "Id2" int, + PRIMARY KEY ("Id2", "Id1") +) +""", + [], + [], + dbModel => + { + var pk = dbModel.Tables.Single().PrimaryKey!; + + Assert.Equal("public", pk.Table!.Schema); + Assert.Equal("CompositePrimaryKeyTable", pk.Table.Name); + Assert.Equal(["Id2", "Id1"], pk.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "CompositePrimaryKeyTable" + """); + + [Fact] + public void Set_primary_key_name_from_index() + => Test( + """ +CREATE TABLE "PrimaryKeyName" ( + "Id1" int, + "Id2" int, + CONSTRAINT "MyPK" PRIMARY KEY ( "Id2" ) +) +""", + [], + [], + dbModel => + { + var pk = dbModel.Tables.Single().PrimaryKey!; + + Assert.Equal("public", pk.Table!.Schema); + Assert.Equal("PrimaryKeyName", pk.Table.Name); + Assert.StartsWith("MyPK", pk.Name); + Assert.Equal(["Id2"], pk.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "PrimaryKeyName" + """); + + #endregion + + #region UniqueConstraintFacets + + [Fact] + public void Create_composite_unique_constraint() + => Test( + """ +CREATE TABLE "CompositeUniqueConstraintTable" ( + "Id1" int, + "Id2" int, + CONSTRAINT "UX" UNIQUE ("Id2", "Id1") +); +""", + [], + [], + dbModel => + { + var uniqueConstraint = Assert.Single(dbModel.Tables.Single().UniqueConstraints); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", uniqueConstraint.Table.Schema); + Assert.Equal("CompositeUniqueConstraintTable", uniqueConstraint.Table.Name); + Assert.Equal("UX", uniqueConstraint.Name); + Assert.Equal(["Id2", "Id1"], uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "CompositeUniqueConstraintTable" + """); + + [Fact] + public void Set_unique_constraint_name_from_index() + => Test( + """ +CREATE TABLE "UniqueConstraintName" ( + "Id1" int, + "Id2" int, + CONSTRAINT "MyUC" UNIQUE ( "Id2" ) +); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + var uniqueConstraint = Assert.Single(table.UniqueConstraints); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", uniqueConstraint.Table.Schema); + Assert.Equal("UniqueConstraintName", uniqueConstraint.Table.Name); + Assert.Equal("MyUC", uniqueConstraint.Name); + Assert.Equal(["Id2"], uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); + Assert.Empty(table.Indexes); + }, + """ + DROP TABLE "UniqueConstraintName" + """); + + #endregion + + #region IndexFacets + + [Fact] + public void Create_composite_index() + => Test( + """ +CREATE TABLE "CompositeIndexTable" ( + "Id1" int, + "Id2" int +); + +CREATE INDEX "IX_COMPOSITE" ON "CompositeIndexTable" ( "Id2", "Id1" ); +""", + [], + [], + dbModel => + { + var index = Assert.Single(dbModel.Tables.Single().Indexes); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", index.Table!.Schema); + Assert.Equal("CompositeIndexTable", index.Table.Name); + Assert.Equal("IX_COMPOSITE", index.Name); + Assert.Equal(["Id2", "Id1"], index.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "CompositeIndexTable" + """); + + [Fact] + public void Set_unique_true_for_unique_index() + => Test( + """ +CREATE TABLE "UniqueIndexTable" ( + "Id1" int, + "Id2" int +); + +CREATE UNIQUE INDEX "IX_UNIQUE" ON "UniqueIndexTable" ( "Id2" ); +""", + [], + [], + dbModel => + { + var index = Assert.Single(dbModel.Tables.Single().Indexes); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", index.Table!.Schema); + Assert.Equal("UniqueIndexTable", index.Table.Name); + Assert.Equal("IX_UNIQUE", index.Name); + Assert.True(index.IsUnique); + Assert.Null(index.Filter); + Assert.Equal(["Id2"], index.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "UniqueIndexTable" + """); + + [Fact] + public void Set_filter_for_filtered_index() + => Test( + """ +CREATE TABLE "FilteredIndexTable" ( + "Id1" int, + "Id2" int NULL +); + +CREATE UNIQUE INDEX "IX_UNIQUE" ON "FilteredIndexTable" ( "Id2" ) WHERE "Id2" > 10; +""", + [], + [], + dbModel => + { + var index = Assert.Single(dbModel.Tables.Single().Indexes); + + // ReSharper disable once PossibleNullReferenceException + 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(["Id2"], index.Columns.Select(ic => ic.Name).ToList()); + }, + """ + DROP TABLE "FilteredIndexTable" + """); + + #endregion + + #region ForeignKeyFacets + + [Fact] + public void Create_composite_foreign_key() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id1" int, + "Id2" int, + PRIMARY KEY ("Id1", "Id2") +); + +CREATE TABLE "DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeyId1" int, + "ForeignKeyId2" int, + FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "PrincipalTable"("Id1", "Id2") ON DELETE CASCADE +); +""", + [], + [], + dbModel => + { + var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", fk.Table.Schema); + Assert.Equal("DependentTable", fk.Table.Name); + Assert.Equal("public", fk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); + 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); + }, + """ +DROP TABLE "DependentTable"; +DROP TABLE "PrincipalTable"; +"""); + + [Fact] + public void Create_multiple_foreign_key_in_same_table() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id" int PRIMARY KEY +); + +CREATE TABLE "AnotherPrincipalTable" ( + "Id" int PRIMARY KEY +); + +CREATE TABLE "DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeyId1" int, + "ForeignKeyId2" int, + FOREIGN KEY ("ForeignKeyId1") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE, + FOREIGN KEY ("ForeignKeyId2") REFERENCES "AnotherPrincipalTable"("Id") ON DELETE CASCADE +); +""", + [], + [], + dbModel => + { + var foreignKeys = dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys; + + Assert.Equal(2, foreignKeys.Count); + + var principalFk = Assert.Single(foreignKeys, f => f.PrincipalTable.Name == "PrincipalTable"); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", principalFk.Table.Schema); + Assert.Equal("DependentTable", principalFk.Table.Name); + Assert.Equal("public", principalFk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", principalFk.PrincipalTable.Name); + 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, f => f.PrincipalTable.Name == "AnotherPrincipalTable"); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", anotherPrincipalFk.Table.Schema); + Assert.Equal("DependentTable", anotherPrincipalFk.Table.Name); + Assert.Equal("public", anotherPrincipalFk.PrincipalTable.Schema); + Assert.Equal("AnotherPrincipalTable", anotherPrincipalFk.PrincipalTable.Name); + 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); + }, + """ +DROP TABLE "DependentTable"; +DROP TABLE "AnotherPrincipalTable"; +DROP TABLE "PrincipalTable"; +"""); + + [Fact] + public void Create_foreign_key_referencing_unique_constraint() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id1" int, + "Id2" int UNIQUE +); + +CREATE TABLE "DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeyId" int, + FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id2") ON DELETE CASCADE +); +""", + [], + [], + dbModel => + { + var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", fk.Table.Schema); + Assert.Equal("DependentTable", fk.Table.Name); + Assert.Equal("public", fk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); + 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); + }, + """ +DROP TABLE "DependentTable"; +DROP TABLE "PrincipalTable"; +"""); + + [Fact] + public void Set_name_for_foreign_key() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id" int PRIMARY KEY +); + +CREATE TABLE "DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeyId" int, + CONSTRAINT "MYFK" FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE +); +""", + [], + [], + dbModel => + { + var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); + + // ReSharper disable once PossibleNullReferenceException + Assert.Equal("public", fk.Table.Schema); + Assert.Equal("DependentTable", fk.Table.Name); + Assert.Equal("public", fk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); + 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); + }, + """ +DROP TABLE "DependentTable"; +DROP TABLE "PrincipalTable"; +"""); + + [Fact] + public void Set_referential_action_for_foreign_key() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id" int PRIMARY KEY +); + +CREATE TABLE "DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeySetNullId" int, + "ForeignKeyCascadeId" int, + "ForeignKeyNoActionId" int, + "ForeignKeyRestrictId" int, + "ForeignKeySetDefaultId" int, + FOREIGN KEY ("ForeignKeySetNullId") REFERENCES "PrincipalTable"("Id") ON DELETE SET NULL, + FOREIGN KEY ("ForeignKeyCascadeId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE, + FOREIGN KEY ("ForeignKeyNoActionId") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION, + FOREIGN KEY ("ForeignKeyRestrictId") REFERENCES "PrincipalTable"("Id") ON DELETE RESTRICT, + FOREIGN KEY ("ForeignKeySetDefaultId") REFERENCES "PrincipalTable"("Id") ON DELETE SET DEFAULT +); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(t => t.Name == "DependentTable"); + + foreach (var fk in table.ForeignKeys) + { + Assert.Equal("public", fk.Table.Schema); + Assert.Equal("DependentTable", fk.Table.Name); + Assert.Equal("public", fk.PrincipalTable.Schema); + Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); + Assert.Equal(["Id"], fk.PrincipalColumns.Select(ic => ic.Name).ToList()); + } + + Assert.Equal( + ReferentialAction.SetNull, table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeySetNullId").OnDelete); + Assert.Equal( + ReferentialAction.Cascade, table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeyCascadeId").OnDelete); + Assert.Equal( + ReferentialAction.NoAction, + table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeyNoActionId").OnDelete); + Assert.Equal( + ReferentialAction.Restrict, + table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeyRestrictId").OnDelete); + Assert.Equal( + ReferentialAction.SetDefault, + table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeySetDefaultId").OnDelete); + }, + """ +DROP TABLE "DependentTable"; +DROP TABLE "PrincipalTable"; +"""); + + #endregion + + #region Warnings + + [Fact] + public void Warn_missing_schema() + => Test( + """ +CREATE TABLE "Blank" ("Id" int) +""", + [], + ["MySchema"], + dbModel => + { + Assert.Empty(dbModel.Tables); + + var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + + Assert.Equal(GaussDBResources.LogMissingSchema(new TestLogger()).EventId, Id); + Assert.Equal( + GaussDBResources.LogMissingSchema(new TestLogger()).GenerateMessage("MySchema"), Message); + }, + """ + DROP TABLE "Blank" + """); + + [Fact] + public void Warn_missing_table() + => Test( + """ +CREATE TABLE "Blank" ("Id" int) +""", + ["MyTable"], + [], + dbModel => + { + Assert.Empty(dbModel.Tables); + + var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + + Assert.Equal(GaussDBResources.LogMissingTable(new TestLogger()).EventId, Id); + Assert.Equal( + GaussDBResources.LogMissingTable(new TestLogger()).GenerateMessage("MyTable"), Message); + }, + """ + DROP TABLE "Blank" + """); + + [Fact] + public void Warn_missing_principal_table_for_foreign_key() + => Test( + """ +CREATE TABLE "PrincipalTable" ( + "Id" int PRIMARY KEY +); + +CREATE TABLE "DependentTable" ( + "Id" int PRIMARY KEY, + "ForeignKeyId" int, + CONSTRAINT "MYFK" FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE +); +""", + ["DependentTable"], + [], + _ => + { + var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); + + Assert.Equal(GaussDBResources.LogPrincipalTableNotInSelectionSet(new TestLogger()).EventId, Id); + Assert.Equal( + GaussDBResources.LogPrincipalTableNotInSelectionSet(new TestLogger()).GenerateMessage( + "MYFK", "public.DependentTable", "public.PrincipalTable"), Message); + }, + """ +DROP TABLE "DependentTable"; +DROP TABLE "PrincipalTable"; +"""); + + #endregion + + #region GaussDB-specific + + [Fact] + public void SequenceSerial() + => Test( + """ +CREATE TABLE serial_sequence (id serial PRIMARY KEY); +CREATE TABLE "SerialSequence" ("Id" serial PRIMARY KEY); +CREATE SCHEMA my_schema; +CREATE TABLE my_schema.serial_sequence_in_schema (Id serial PRIMARY KEY); +CREATE TABLE my_schema."SerialSequenceInSchema" ("Id" serial PRIMARY KEY); +""", + [], + [], + dbModel => + { + // Sequences which belong to a serial column should not get reverse engineered as separate sequences + Assert.Empty(dbModel.Sequences); + + // Now make sure the field itself is properly reverse-engineered. + foreach (var column in dbModel.Tables.Select(t => t.Columns.Single())) + { + Assert.Equal(ValueGenerated.OnAdd, column.ValueGenerated); + Assert.Null(column.DefaultValueSql); + Assert.Equal( + GaussDBValueGenerationStrategy.SerialColumn, + (GaussDBValueGenerationStrategy?)column[GaussDBAnnotationNames.ValueGenerationStrategy]); + } + }, + """ +DROP TABLE serial_sequence; +DROP TABLE "SerialSequence"; +DROP SCHEMA my_schema CASCADE +"""); + + [Fact] + public void SequenceNonSerial() + => Test( + """ +CREATE SEQUENCE "SomeSequence"; +CREATE TABLE "NonSerialSequence" ("Id" integer PRIMARY KEY DEFAULT nextval('"SomeSequence"')) +""", + [], + [], + dbModel => + { + var column = dbModel.Tables.Single().Columns.Single(); + Assert.Equal("""nextval('"SomeSequence"'::regclass)""", column.DefaultValueSql); + // GaussDB 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. + Assert.Null(column.ValueGenerated); + + Assert.Single(dbModel.Sequences, s => s.Name == "SomeSequence"); + }, + """ +DROP TABLE "NonSerialSequence"; +DROP SEQUENCE "SomeSequence"; +"""); + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void Identity() + => Test( + """ +CREATE TABLE identity ( + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + a int GENERATED ALWAYS AS IDENTITY, + b int GENERATED BY DEFAULT AS IDENTITY +) +""", + [], + [], + dbModel => + { + var idIdentityAlways = dbModel.Tables.Single().Columns.Single(c => c.Name == "id"); + Assert.Equal(ValueGenerated.OnAdd, idIdentityAlways.ValueGenerated); + Assert.Null(idIdentityAlways.DefaultValueSql); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityAlwaysColumn, + (GaussDBValueGenerationStrategy?)idIdentityAlways[GaussDBAnnotationNames.ValueGenerationStrategy]); + + var identityAlways = dbModel.Tables.Single().Columns.Single(c => c.Name == "a"); + Assert.Equal(ValueGenerated.OnAdd, identityAlways.ValueGenerated); + Assert.Null(identityAlways.DefaultValueSql); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityAlwaysColumn, + (GaussDBValueGenerationStrategy?)identityAlways[GaussDBAnnotationNames.ValueGenerationStrategy]); + + var identityByDefault = dbModel.Tables.Single().Columns.Single(c => c.Name == "b"); + Assert.Equal(ValueGenerated.OnAdd, identityByDefault.ValueGenerated); + Assert.Null(identityByDefault.DefaultValueSql); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, + (GaussDBValueGenerationStrategy?)identityByDefault[GaussDBAnnotationNames.ValueGenerationStrategy]); + }, + "DROP TABLE identity"); + + [ConditionalFact] + [MinimumPostgresVersion(10, 0)] + public void Identity_with_sequence_options_all() + => Test( + """ +CREATE TABLE identity ( + with_options int GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 MINVALUE 3 MAXVALUE 2000 CYCLE CACHE 10), + without_options int GENERATED BY DEFAULT AS IDENTITY, + bigint_without_options bigint GENERATED BY DEFAULT AS IDENTITY, + smallint_without_options smallint GENERATED BY DEFAULT AS IDENTITY +) +""", + [], + [], + dbModel => + { + var withOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "with_options"); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, + (GaussDBValueGenerationStrategy?)withOptions[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Equal( + new IdentitySequenceOptionsData + { + StartValue = 5, + MinValue = 3, + MaxValue = 2000, + IncrementBy = 2, + IsCyclic = true, + NumbersToCache = 10 + }.Serialize(), withOptions[GaussDBAnnotationNames.IdentityOptions]); + + var withoutOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "without_options"); + Assert.Equal("integer", withOptions.StoreType); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, + (GaussDBValueGenerationStrategy?)withoutOptions[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(withoutOptions[GaussDBAnnotationNames.IdentityOptions]); + + var bigintWithoutOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "bigint_without_options"); + Assert.Equal("bigint", bigintWithoutOptions.StoreType); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, + (GaussDBValueGenerationStrategy?)bigintWithoutOptions[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(bigintWithoutOptions[GaussDBAnnotationNames.IdentityOptions]); + + var smallintWithoutOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "smallint_without_options"); + Assert.Equal("smallint", smallintWithoutOptions.StoreType); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, + (GaussDBValueGenerationStrategy?)smallintWithoutOptions[GaussDBAnnotationNames.ValueGenerationStrategy]); + Assert.Null(smallintWithoutOptions[GaussDBAnnotationNames.IdentityOptions]); + }, + "DROP TABLE identity"); + + [Fact] + public void Column_collation_is_set() + => Test( + """ +CREATE TABLE columns_with_collation ( + id int, + default_collation TEXT, + non_default_collation TEXT COLLATE "POSIX" +); +""", + [], + [], + dbModel => + { + var columns = dbModel.Tables.Single().Columns; + + Assert.Null(columns.Single(c => c.Name == "default_collation").Collation); + Assert.Equal("POSIX", columns.Single(c => c.Name == "non_default_collation").Collation); + }, + @"DROP TABLE columns_with_collation"); + + [ConditionalFact] + public void Default_database_collation_is_not_scaffolded() + => Test( + @"-- Empty database", + [], + [], + dbModel => Assert.Null(dbModel.Collation), + @""); + + [Fact] + public void Index_method() + => Test( + """ +CREATE TABLE "IndexMethod" (a int, b int); +CREATE INDEX ix_a ON "IndexMethod" USING hash (a); +CREATE INDEX ix_b ON "IndexMethod" (b); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + Assert.Equal(2, table.Indexes.Count); + + var methodIndex = table.Indexes.Single(i => i.Name == "ix_a"); + Assert.Equal("hash", methodIndex[GaussDBAnnotationNames.IndexMethod]); + + // It's cleaner to always output the index method on the database model, + // even when it's btree (the default); + // GaussDBAnnotationCodeGenerator can then omit it as by-convention. + // However, because of https://github.com/aspnet/EntityFrameworkCore/issues/11846 we omit + // the annotation from the model entirely. + var noMethodIndex = table.Indexes.Single(i => i.Name == "ix_b"); + Assert.Null(noMethodIndex.FindAnnotation(GaussDBAnnotationNames.IndexMethod)); + //Assert.Equal("btree", noMethodIndex.FindAnnotation(GaussDBAnnotationNames.IndexMethod).Value); + }, + """ + DROP TABLE "IndexMethod" + """); + + [Fact] + public void Index_operators() + => Test( + """ +CREATE TABLE "IndexOperators" (a text, b text); +CREATE INDEX ix_with ON "IndexOperators" (a, b varchar_pattern_ops); +CREATE INDEX ix_without ON "IndexOperators" (a, b); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); + Assert.Equal(new[] { null, "varchar_pattern_ops" }, indexWith[GaussDBAnnotationNames.IndexOperators]); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Null(indexWithout.FindAnnotation(GaussDBAnnotationNames.IndexOperators)); + }, + """ + DROP TABLE "IndexOperators" + """); + + [Fact] + public void Index_collation() + => Test( + """ +CREATE TABLE "IndexCollation" (a text, b text); +CREATE INDEX ix_with ON "IndexCollation" (a, b COLLATE "POSIX"); +CREATE INDEX ix_without ON "IndexCollation" (a, b); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); + Assert.Equal(new[] { null, "POSIX" }, indexWith[RelationalAnnotationNames.Collation]); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Null(indexWithout.FindAnnotation(RelationalAnnotationNames.Collation)); + }, + """ + DROP TABLE "IndexCollation" + """); + + [Theory] + [InlineData("gin", new bool[0])] + [InlineData("gist", new bool[0])] + [InlineData("hash", new bool[0])] + [InlineData("brin", new bool[0])] + [InlineData("btree", new[] { false, true })] + public void Index_IsDescending(string method, bool[] expected) + => Test( + """ +CREATE TABLE "IndexSortOrder" (a text, b text, c tsvector); +CREATE INDEX ix_gin ON "IndexSortOrder" USING gin (c); +CREATE INDEX ix_gist ON "IndexSortOrder" USING gist (c); +CREATE INDEX ix_hash ON "IndexSortOrder" USING hash (a); +CREATE INDEX ix_brin ON "IndexSortOrder" USING brin (a); +CREATE INDEX ix_btree ON "IndexSortOrder" USING btree (a ASC, b DESC); +CREATE INDEX ix_without ON "IndexSortOrder" (a, b); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == $"ix_{method}"); + // Assert.True(indexWith.IsDescending.SequenceEqual(expected)); + Assert.Equal(expected, indexWith.IsDescending); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Equal([false, false], indexWithout.IsDescending); + }, + """ + DROP TABLE "IndexSortOrder" + """); + + [Fact] + public void Index_null_sort_order() + => Test( + """ +CREATE TABLE "IndexNullSortOrder" (a text, b text); +CREATE INDEX ix_with ON "IndexNullSortOrder" (a NULLS FIRST, b DESC NULLS LAST); +CREATE INDEX ix_without ON "IndexNullSortOrder" (a, b); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); + Assert.Equal( + new[] { NullSortOrder.NullsFirst, NullSortOrder.NullsLast }, + indexWith[GaussDBAnnotationNames.IndexNullSortOrder]); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Null(indexWithout.FindAnnotation(GaussDBAnnotationNames.IndexNullSortOrder)); + }, + """ + DROP TABLE "IndexNullSortOrder" + """); + + [ConditionalFact] + [MinimumPostgresVersion(11, 0)] + public void Index_covering() + => Test( + """ +CREATE TABLE "IndexCovering" (a text, b text, c text); +CREATE INDEX ix_with ON "IndexCovering" (a) INCLUDE (b, c); +CREATE INDEX ix_without ON "IndexCovering" (a, b, c); +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); + Assert.Equal("a", indexWith.Columns.Single().Name); + // Scaffolding included/covered properties is currently blocked, see #2194 + Assert.Null(indexWith.FindAnnotation(GaussDBAnnotationNames.IndexInclude)); + // Assert.Equal(new[] { "b", "c" }, indexWith.FindAnnotation(GaussDBAnnotationNames.IndexInclude).Value); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Equal(new[] { "a", "b", "c" }, indexWithout.Columns.Select(i => i.Name).ToArray()); + Assert.Null(indexWithout.FindAnnotation(GaussDBAnnotationNames.IndexInclude)); + }, + """ + DROP TABLE "IndexCovering" + """); + + [ConditionalFact] + [MinimumPostgresVersion(15, 0)] + public void Index_are_nulls_distinct() + => Test( + """ +CREATE TABLE "IndexNullsDistinct" (a text); +CREATE INDEX "IX_NullsDistinct" ON "IndexNullsDistinct" (a); +CREATE INDEX "IX_NullsNotDistinct" ON "IndexNullsDistinct" (a) NULLS NOT DISTINCT; +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + + Assert.Null( + Assert.Single(table.Indexes, i => i.Name == "IX_NullsDistinct")[GaussDBAnnotationNames.NullsDistinct]); + + Assert.Equal( + false, + Assert.Single(table.Indexes, i => i.Name == "IX_NullsNotDistinct")[GaussDBAnnotationNames.NullsDistinct]); + }, + """ + DROP TABLE "IndexNullsDistinct" + """); + + [Fact] + public void Comments() + => Test( + """ +CREATE TABLE comment (a int); +COMMENT ON TABLE comment IS 'table comment'; +COMMENT ON COLUMN comment.a IS 'column comment' +""", + [], + [], + dbModel => + { + var table = dbModel.Tables.Single(); + Assert.Equal("table comment", table.Comment); + Assert.Equal("column comment", table.Columns.Single().Comment); + }, + "DROP TABLE comment"); + + [ConditionalFact] + [MinimumPostgresVersion(11, 0)] + public void Sequence_types() + => Test( + """ +CREATE SEQUENCE "SmallIntSequence" AS smallint; +CREATE SEQUENCE "IntSequence" AS int; +CREATE SEQUENCE "BigIntSequence" AS bigint; +""", + [], + [], + dbModel => + { + var smallSequence = dbModel.Sequences.Single(s => s.Name == "SmallIntSequence"); + Assert.Equal("smallint", smallSequence.StoreType); + var intSequence = dbModel.Sequences.Single(s => s.Name == "IntSequence"); + Assert.Equal("integer", intSequence.StoreType); + var bigSequence = dbModel.Sequences.Single(s => s.Name == "BigIntSequence"); + Assert.Equal("bigint", bigSequence.StoreType); + }, + """ +DROP SEQUENCE "SmallIntSequence"; +DROP SEQUENCE "IntSequence"; +DROP SEQUENCE "BigIntSequence"; +"""); + + [Fact] + public void Dropped_columns() + => Test( + """ +CREATE TABLE foo (id int PRIMARY KEY); +ALTER TABLE foo DROP COLUMN id; +ALTER TABLE foo ADD COLUMN id2 int PRIMARY KEY; +""", + [], + [], + dbModel => + { + Assert.Single(dbModel.Tables.Single().Columns); + }, + "DROP TABLE foo"); + + [Fact] + public void Postgres_extensions() + => Test( + """ +DROP EXTENSION IF EXISTS postgis; +CREATE EXTENSION hstore; +CREATE EXTENSION pgcrypto SCHEMA db2; +""", + [], + [], + dbModel => + { + var extensions = dbModel.GetPostgresExtensions(); + Assert.Collection( + extensions.OrderBy(e => e.Name), + e => + { + Assert.Equal("hstore", e.Name); + Assert.Equal("public", e.Schema); + }, + e => + { + Assert.Equal("pgcrypto", e.Name); + Assert.Equal("db2", e.Schema); + }); + }, + "DROP EXTENSION hstore; DROP EXTENSION pgcrypto"); + + [Fact] + public void Enums() + => Test( + """ +CREATE TYPE mood AS ENUM ('happy', 'sad'); +CREATE TYPE db2.mood AS ENUM ('excited', 'depressed'); +CREATE TABLE foo (mood mood UNIQUE); +""", + [], + [], + dbModel => + { + var enums = dbModel.GetPostgresEnums(); + Assert.Equal(2, enums.Count); + + var mood = enums.Single(e => e.Schema is null); + Assert.Equal("mood", mood.Name); + Assert.Equal(["happy", "sad"], mood.Labels); + + var mood2 = enums.Single(e => e.Schema == "db2"); + Assert.Equal("mood", mood2.Name); + Assert.Equal(["excited", "depressed"], mood2.Labels); + + var table = Assert.Single(dbModel.Tables); + Assert.NotNull(table); + + // Enum columns are left out of the model for now (a warning is logged). + Assert.Empty(table.Columns); + // Constraints and indexes over enum columns also need to be left out + Assert.Empty(table.UniqueConstraints); + Assert.Empty(table.Indexes); + }, + """ +DROP TABLE foo; +DROP TYPE mood; +DROP TYPE db2.mood; +"""); + + [Fact] + public void Bug453() + => Test( + """ +CREATE TYPE mood AS ENUM ('happy', 'sad'); +CREATE TABLE foo (mood mood, some_num int UNIQUE); +CREATE TABLE bar (foreign_key int REFERENCES foo(some_num)); +""", + [], + [], + // 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), + """ +DROP TABLE bar; +DROP TABLE foo; +DROP TYPE mood; +"""); + + [Fact] + public void Column_default_type_names_are_scaffolded() + => Test( + """ +CREATE TABLE column_types ( + smallint smallint, + integer integer, + bigint bigint, + real real, + "double precision" double precision, + money money, + numeric numeric, + boolean boolean, + bytea bytea, + uuid uuid, + text text, + jsonb jsonb, + json json, + "character varying" character varying, + "character(1)" character, + "character(2)" character(2), + "timestamp without time zone" timestamp, + "timestamp with time zone" timestamptz, + "time without time zone" time, + "time with time zone" timetz, + interval interval, + macaddr macaddr, + inet inet, + "bit(1)" bit, + "bit varying" varbit, + point point, + line line +) +""", + [], + [], + dbModel => + { + var options = new GaussDBSingletonOptions(); + options.Initialize(new DbContextOptionsBuilder().Options); + + var typeMappingSource = new GaussDBTypeMappingSource( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + [] + ), + new RelationalTypeMappingSourceDependencies([]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + options); + + foreach (var column in dbModel.Tables.Single().Columns) + { + Assert.Equal(column.Name, column.StoreType); + Assert.Equal( + column.StoreType, + typeMappingSource.FindMapping(column.StoreType!)!.StoreType + ); + } + }, + "DROP TABLE column_types"); + + [ConditionalFact] + [RequiresPostgis] + public void System_tables_are_ignored() + => Test( + """ +DROP EXTENSION IF EXISTS postgis; +CREATE EXTENSION postgis; +""", + [], + [], + dbModel => Assert.Empty(dbModel.Tables), + "DROP EXTENSION postgis"); + + #endregion + + private void Test( + string createSql, + IEnumerable tables, + IEnumerable schemas, + Action asserter, + string? cleanupSql) + { + Fixture.TestStore.ExecuteNonQuery(createSql); + + try + { + var databaseModelFactory = new GaussDBDatabaseModelFactory( + new DiagnosticsLogger( + Fixture.ListLoggerFactory, + new LoggingOptions(), + new DiagnosticListener("Fake"), + new GaussDBLoggingDefinitions(), + new NullDbContextLogger())); + + var databaseModel = databaseModelFactory.Create( + Fixture.TestStore.ConnectionString, + new DatabaseModelFactoryOptions(tables, schemas)); + Assert.NotNull(databaseModel); + asserter(databaseModel); + } + finally + { + if (!string.IsNullOrEmpty(cleanupSql)) + { + Fixture.TestStore.ExecuteNonQuery(cleanupSql); + } + } + } + + public class GaussDBDatabaseModelFixture : SharedStoreFixtureBase + { + protected override string StoreName { get; } = nameof(GaussDBDatabaseModelFactoryTest); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public new GaussDBTestStore TestStore + => (GaussDBTestStore)base.TestStore; + + 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" + """); + } + + protected override bool ShouldLogCategory(string logCategory) + => logCategory == DbLoggerCategory.Scaffolding.Name; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/SeedingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/SeedingGaussDBTest.cs new file mode 100644 index 0000000000..3a8d91d467 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/SeedingGaussDBTest.cs @@ -0,0 +1,16 @@ +namespace Microsoft.EntityFrameworkCore; + +public class SeedingGaussDBTest : SeedingTestBase +{ + protected override TestStore TestStore + => GaussDBTestStore.Create("SeedingTest"); + + protected override SeedingContext CreateContextWithEmptyDatabase(string testId) + => new SeedingGaussDBContext(testId); + + protected class SeedingGaussDBContext(string testId) : SeedingContext(testId) + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(GaussDBTestStore.CreateConnectionString($"Seeds{TestId}")); + } +} diff --git a/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs b/test/EFCore.GaussDB.FunctionalTests/SequenceEndToEndTest.cs similarity index 93% rename from test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs rename to test/EFCore.GaussDB.FunctionalTests/SequenceEndToEndTest.cs index 7fea096121..a860c2c1d2 100644 --- a/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/SequenceEndToEndTest.cs @@ -8,7 +8,7 @@ public class SequenceEndToEndTest : IAsyncLifetime public void Can_use_sequence_end_to_end() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using (var context = new BronieContext(serviceProvider, TestStore.Name)) @@ -22,7 +22,7 @@ public void Can_use_sequence_end_to_end() // Use a different service provider so a different generator is used but with // the same server sequence. serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); AddEntities(serviceProvider, TestStore.Name); @@ -57,7 +57,7 @@ private static void AddEntities(IServiceProvider serviceProvider, string name) public void Can_use_sequence_end_to_end_on_multiple_databases() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); var dbOne = TestStore.Name + "1"; @@ -77,7 +77,7 @@ public void Can_use_sequence_end_to_end_on_multiple_databases() // Use a different service provider so a different generator is used but with // the same server sequence. serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); AddEntitiesToMultipleContexts(serviceProvider, dbOne, dbTwo); @@ -124,7 +124,7 @@ private static void AddEntitiesToMultipleContexts( public async Task Can_use_sequence_end_to_end_async() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); await using (var context = new BronieContext(serviceProvider, TestStore.Name)) @@ -138,7 +138,7 @@ public async Task Can_use_sequence_end_to_end_async() // Use a different service provider so a different generator is used but with // the same server sequence. serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); await AddEntitiesAsync(serviceProvider, TestStore.Name); @@ -173,7 +173,7 @@ await context.AddAsync( public async Task Can_use_sequence_end_to_end_from_multiple_contexts_concurrently_async() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); await using (var context = new BronieContext(serviceProvider, TestStore.Name)) @@ -213,7 +213,7 @@ public async Task Can_use_sequence_end_to_end_from_multiple_contexts_concurrentl public void Can_use_explicit_values() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using (var context = new BronieContext(serviceProvider, TestStore.Name)) @@ -227,7 +227,7 @@ public void Can_use_explicit_values() // Use a different service provider so a different generator is used but with // the same server sequence. serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); AddEntitiesWithIds(serviceProvider, 4, TestStore.Name); @@ -270,7 +270,7 @@ private class BronieContext(IServiceProvider serviceProvider, string databaseNam protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseInternalServiceProvider(serviceProvider) - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); + .UseGaussDB(GaussDBTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( @@ -291,7 +291,7 @@ private class Pegasus public void Can_use_sequence_with_nullable_key_end_to_end() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using (var context = new NullableBronieContext(serviceProvider, TestStore.Name, true)) @@ -319,7 +319,7 @@ public void Can_use_sequence_with_nullable_key_end_to_end() public void Can_use_identity_with_nullable_key_end_to_end() { var serviceProvider = new ServiceCollection() - .AddEntityFrameworkNpgsql() + .AddEntityFrameworkGaussDB() .BuildServiceProvider(); using (var context = new NullableBronieContext(serviceProvider, TestStore.Name, false)) @@ -365,7 +365,7 @@ private class NullableBronieContext(IServiceProvider serviceProvider, string dat protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseInternalServiceProvider(serviceProvider) - .UseNpgsql(NpgsqlTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); + .UseGaussDB(GaussDBTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( @@ -389,10 +389,10 @@ private class Unicon public string Name { get; set; } = null!; } - protected NpgsqlTestStore TestStore { get; private set; } = null!; + protected GaussDBTestStore TestStore { get; private set; } = null!; public async Task InitializeAsync() - => TestStore = await NpgsqlTestStore.CreateInitializedAsync("SequenceEndToEndTest"); + => TestStore = await GaussDBTestStore.CreateInitializedAsync("SequenceEndToEndTest"); public async Task DisposeAsync() => await TestStore.DisposeAsync(); diff --git a/test/EFCore.GaussDB.FunctionalTests/SerializationGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/SerializationGaussDBTest.cs new file mode 100644 index 0000000000..893445faee --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/SerializationGaussDBTest.cs @@ -0,0 +1,3 @@ +namespace Microsoft.EntityFrameworkCore; + +public class SerializationGaussDBTest(F1BytesGaussDBFixture fixture) : SerializationTestBase(fixture); diff --git a/test/EFCore.GaussDB.FunctionalTests/SpatialGaussDBFixture.cs b/test/EFCore.GaussDB.FunctionalTests/SpatialGaussDBFixture.cs new file mode 100644 index 0000000000..5ecf01cda7 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/SpatialGaussDBFixture.cs @@ -0,0 +1,32 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore; + +public class SpatialGaussDBFixture : SpatialFixtureBase +{ + // We instruct the test store to pass a connection string to UseGaussDB() instead of a DbConnection - that's required to allow + // EF's UseNetTopologySuite() to function properly and instantiate an GaussDBDataSource internally. + protected override ITestStoreFactory TestStoreFactory + => new GaussDBTestStoreFactory(useConnectionString: true); + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection) + .AddEntityFrameworkGaussDBNetTopologySuite(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new GaussDBDbContextOptionsBuilder(optionsBuilder) + .UseNetTopologySuite() + .SetPostgresVersion(TestEnvironment.PostgresVersion); + + return optionsBuilder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("uuid-ossp"); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/SpatialGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/SpatialGaussDBTest.cs new file mode 100644 index 0000000000..6f784c0e91 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/SpatialGaussDBTest.cs @@ -0,0 +1,13 @@ +namespace Microsoft.EntityFrameworkCore; + +[RequiresPostgis] +public class SpatialGaussDBTest(SpatialGaussDBFixture fixture) : SpatialTestBase(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 SpatialGaussDBFixture 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.GaussDB.FunctionalTests/StoreGeneratedFixupGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/StoreGeneratedFixupGaussDBTest.cs new file mode 100644 index 0000000000..acc1b942c8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/StoreGeneratedFixupGaussDBTest.cs @@ -0,0 +1,186 @@ +namespace Microsoft.EntityFrameworkCore; + +public class StoreGeneratedFixupGaussDBTest(StoreGeneratedFixupGaussDBTest.StoreGeneratedFixupGaussDBFixture fixture) + : StoreGeneratedFixupRelationalTestBase(fixture) +{ + [Fact] + public Task Temp_values_are_replaced_on_save() + => ExecuteWithStrategyInTransactionAsync( + async context => + { + var entry = context.Add(new TestTemp()); + + Assert.True(entry.Property(e => e.Id).IsTemporary); + Assert.False(entry.Property(e => e.NotId).IsTemporary); + + var tempValue = entry.Property(e => e.Id).CurrentValue; + + await context.SaveChangesAsync(); + + Assert.False(entry.Property(e => e.Id).IsTemporary); + Assert.NotEqual(tempValue, entry.Property(e => e.Id).CurrentValue); + }); + + protected override void MarkIdsTemporary(DbContext context, object dependent, object principal) + { + var entry = context.Entry(dependent); + entry.Property("Id1").IsTemporary = true; + entry.Property("Id2").IsTemporary = true; + + foreach (var property in entry.Properties) + { + if (property.Metadata.IsForeignKey()) + { + property.IsTemporary = true; + } + } + + entry = context.Entry(principal); + entry.Property("Id1").IsTemporary = true; + entry.Property("Id2").IsTemporary = true; + } + + protected override void MarkIdsTemporary(DbContext context, object game, object level, object item) + { + var entry = context.Entry(game); + entry.Property("Id").IsTemporary = true; + + entry = context.Entry(item); + entry.Property("Id").IsTemporary = true; + } + + protected override bool EnforcesFKs + => true; + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class StoreGeneratedFixupGaussDBFixture : StoreGeneratedFixupRelationalFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("uuid-ossp"); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id1).ValueGeneratedOnAdd(); + b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); + }); + + modelBuilder.Entity(b => { b.Property(e => e.Id).ValueGeneratedOnAdd(); }); + + modelBuilder.Entity(b => { b.Property(e => e.Id).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); }); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/StoreGeneratedGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/StoreGeneratedGaussDBTest.cs new file mode 100644 index 0000000000..a12f32bf6e --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/StoreGeneratedGaussDBTest.cs @@ -0,0 +1,124 @@ +namespace Microsoft.EntityFrameworkCore; + +public class StoreGeneratedGaussDBTest(StoreGeneratedGaussDBTest.StoreGeneratedGaussDBFixture fixture) + : StoreGeneratedTestBase(fixture) +{ + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class StoreGeneratedGaussDBFixture : StoreGeneratedFixtureBase + { + protected override string StoreName { get; } = "StoreGeneratedTest"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder + .EnableSensitiveDataLogging() + .ConfigureWarnings( + b => b.Default(WarningBehavior.Throw) + .Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning) + .Ignore(RelationalEventId.BoolWithDefaultWarning)); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).UseSerialColumn(); + b.Property(e => e.Identity).HasDefaultValue("Banana Joe"); + b.Property(e => e.IdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); + b.Property(e => e.IdentityReadOnlyAfterSave).HasDefaultValue("Anton"); + b.Property(e => e.AlwaysIdentity).HasDefaultValue("Banana Joe"); + b.Property(e => e.AlwaysIdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); + b.Property(e => e.AlwaysIdentityReadOnlyAfterSave).HasDefaultValue("Anton"); + b.Property(e => e.Computed).HasDefaultValue("Alan"); + b.Property(e => e.ComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); + b.Property(e => e.ComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); + b.Property(e => e.AlwaysComputed).HasDefaultValue("Alan"); + b.Property(e => e.AlwaysComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); + b.Property(e => e.AlwaysComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.OnAdd).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddUseBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddThrowBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddUseBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); + + b.Property(e => e.OnAddOrUpdate).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnAddOrUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); + + b.Property(e => e.OnUpdate).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); + b.Property(e => e.OnUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); + }); + + // In StoreGeneratedSqlServerTest the following are defined with HasComputedValueSql, but that's + // not supported by GaussDB + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableAsNonNullable).HasDefaultValueSql("1"); + b.Property(e => e.NonNullableAsNullable).HasDefaultValueSql("1"); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.TrueDefault).HasDefaultValue(true); + b.Property(e => e.NonZeroDefault).HasDefaultValue(-1); + b.Property(e => e.FalseDefault).HasDefaultValue(false); + b.Property(e => e.ZeroDefault).HasDefaultValue(0); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); + b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); + b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); + b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); + b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); + b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); + b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); + }); + + modelBuilder.Entity().Property(e => e.HasTemp).HasDefaultValue(777); + + modelBuilder.Entity().Property(e => e.Id).UseIdentityColumn(); + + base.OnModelCreating(modelBuilder, context); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/SystemColumnTest.cs b/test/EFCore.GaussDB.FunctionalTests/SystemColumnTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/SystemColumnTest.cs rename to test/EFCore.GaussDB.FunctionalTests/SystemColumnTest.cs index 7165775eca..37af296b77 100644 --- a/test/EFCore.PG.FunctionalTests/SystemColumnTest.cs +++ b/test/EFCore.GaussDB.FunctionalTests/SystemColumnTest.cs @@ -63,7 +63,7 @@ protected override string StoreName => "SystemColumnTest"; protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; + => GaussDBTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.GaussDB.FunctionalTests/TPTTableSplittingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/TPTTableSplittingGaussDBTest.cs new file mode 100644 index 0000000000..01a9fdcef4 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TPTTableSplittingGaussDBTest.cs @@ -0,0 +1,12 @@ +namespace Microsoft.EntityFrameworkCore; + +public class TPTTableSplittingGaussDBTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) + : TPTTableSplittingTestBase(fixture, testOutputHelper) +{ + public override Task Can_insert_dependent_with_just_one_parent() + // This scenario is not valid for TPT + => Task.CompletedTask; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TableSplittingGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/TableSplittingGaussDBTest.cs new file mode 100644 index 0000000000..bda294a299 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TableSplittingGaussDBTest.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.TestModels.TransportationModel; + +namespace Microsoft.EntityFrameworkCore; + +[MinimumPostgresVersion(12, 0)] // Test suite uses computed columns +public class TableSplittingGaussDBTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) + : TableSplittingTestBase(fixture, testOutputHelper) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override async Task ExecuteUpdate_works_for_table_sharing(bool async) + { + await base.ExecuteUpdate_works_for_table_sharing(async); + + AssertSql( + """ +@p='1' + +UPDATE "Vehicles" AS v +SET "SeatingCapacity" = @p +""", + // + """ +SELECT NOT EXISTS ( + SELECT 1 + FROM "Vehicles" AS v + WHERE v."SeatingCapacity" <> 1) +"""); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().ToTable("Vehicles") + .Property(e => e.Computed).HasComputedColumnSql("1", stored: true); + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayEntity.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayEntity.cs diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayQueryContext.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayQueryContext.cs diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayQueryData.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/Array/ArrayQueryData.cs diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/SomeEnum.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/Array/SomeEnum.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestModels/Array/SomeEnum.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/Array/SomeEnum.cs diff --git a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs diff --git a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs similarity index 90% rename from test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs index e39fa25787..62dabe7c56 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs @@ -52,9 +52,9 @@ public static IReadOnlyList CreateNodaTimeTypes() Period = DefaultPeriod, Duration = duration, DateInterval = new DateInterval(localDateTime.Date, localDateTime.Date.PlusDays(4)), // inclusive - LocalDateRange = new NpgsqlRange(localDateTime.Date, localDateTime.Date.PlusDays(5)), // exclusive + LocalDateRange = new GaussDBRange(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), + InstantRange = new GaussDBRange(instant, true, instant + Duration.FromDays(5), false), Long = 1, TimeZoneId = "Europe/Berlin" } diff --git a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs similarity index 87% rename from test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs index fa1faddc48..397479a68b 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs @@ -16,9 +16,9 @@ public class NodaTimeTypes public Period Period { get; set; } = null!; public Duration Duration { get; set; } public DateInterval DateInterval { get; set; } = null!; - public NpgsqlRange LocalDateRange { get; set; } + public GaussDBRange LocalDateRange { get; set; } public Interval Interval { get; set; } - public NpgsqlRange InstantRange { get; set; } + public GaussDBRange InstantRange { get; set; } public long Long { get; set; } public string TimeZoneId { get; set; } = null!; diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs b/test/EFCore.GaussDB.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs similarity index 94% rename from test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs rename to test/EFCore.GaussDB.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs index fa5de8a1e0..5cef5391b8 100644 --- a/test/EFCore.PG.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestModels/Northwind/NorthwindNpgsqlContext.cs @@ -1,6 +1,6 @@ namespace Microsoft.EntityFrameworkCore.TestModels.Northwind; -public class NorthwindNpgsqlContext(DbContextOptions options) : NorthwindRelationalContext(options) +public class NorthwindGaussDBContext(DbContextOptions options) : NorthwindRelationalContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/CollectionExtensions.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/CollectionExtensions.cs similarity index 100% rename from test/EFCore.PG.FunctionalTests/TestUtilities/CollectionExtensions.cs rename to test/EFCore.GaussDB.FunctionalTests/TestUtilities/CollectionExtensions.cs diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDatabaseCleaner.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDatabaseCleaner.cs new file mode 100644 index 0000000000..48b1134e03 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDatabaseCleaner.cs @@ -0,0 +1,177 @@ +using System.Data.Common; +using System.Text; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class GaussDBDatabaseCleaner : RelationalDatabaseCleaner +{ + private readonly GaussDBSqlGenerationHelper _sqlGenerationHelper = new(new RelationalSqlGenerationHelperDependencies()); + + protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) + => new GaussDBDatabaseModelFactory( + new DiagnosticsLogger( + loggerFactory, + new LoggingOptions(), + new DiagnosticListener("Fake"), + new GaussDBLoggingDefinitions(), + new NullDbContextLogger())); + + protected override bool AcceptIndex(DatabaseIndex index) + => false; + + public override void Clean(DatabaseFacade facade) + { + // The following is somewhat hacky + // PostGIS creates some system tables (e.g. spatial_ref_sys) which can't be dropped until the extension + // is dropped. But our tests create some user tables which depend on PostGIS. So we clean out PostGIS + // and all tables that depend on it (CASCADE) before the database model is built. + var creator = facade.GetService(); + var connection = facade.GetService(); + if (creator.Exists()) + { + connection.Open(); + try + { + var conn = (GaussDBConnection)connection.DbConnection; + DropExtensions(conn); + DropTypes(conn); + DropFunctions(conn); + DropCollations(conn); + } + finally + { + connection.Close(); + } + } + + base.Clean(facade); + } + + private void DropExtensions(GaussDBConnection conn) + { + const string getExtensions = "SELECT name FROM pg_available_extensions WHERE installed_version IS NOT NULL AND name <> 'plpgsql'"; + + List extensions; + using (var cmd = new GaussDBCommand(getExtensions, conn)) + { + using var reader = cmd.ExecuteReader(); + extensions = reader.Cast().Select(r => r.GetString(0)).ToList(); + } + + if (extensions.Any()) + { + var dropExtensionsSql = string.Join("", extensions.Select(e => $"DROP EXTENSION \"{e}\" CASCADE;")); + using var cmd = new GaussDBCommand(dropExtensionsSql, conn); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Drop user-defined ranges and enums, cascading to all tables which depend on them + /// + private void DropTypes(GaussDBConnection conn) + { + 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' +"""; + + (string Schema, string Name)[] userDefinedTypes; + using (var cmd = new GaussDBCommand(getUserDefinedRangesEnums, conn)) + { + using var reader = cmd.ExecuteReader(); + userDefinedTypes = reader.Cast().Select(r => (r.GetString(0), r.GetString(1))).ToArray(); + } + + if (userDefinedTypes.Any()) + { + var dropTypes = string.Concat(userDefinedTypes.Select(t => $"""DROP TYPE "{t.Schema}"."{t.Name}" CASCADE;""")); + using var cmd = new GaussDBCommand(dropTypes, conn); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Drop all user-defined functions and procedures + /// + private void DropFunctions(GaussDBConnection conn) + { + 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 + 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'); +"""; + + string dropSql; + using (var cmd = new GaussDBCommand(getUserDefinedFunctions, conn)) + { + using var reader = cmd.ExecuteReader(); + dropSql = string.Join("", reader.Cast().Select(r => r.GetString(0))); + } + + if (dropSql != "") + { + using var cmd = new GaussDBCommand(dropSql, conn); + cmd.ExecuteNonQuery(); + } + } + + private void DropCollations(GaussDBConnection conn) + { + if (conn.PostgreSqlVersion < new Version(9, 1)) + { + return; + } + + 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 nspname <> 'pg_catalog'; +"""; + + (string Schema, string Name)[] userDefinedTypes; + using (var cmd = new GaussDBCommand(getUserCollations, conn)) + { + using var reader = cmd.ExecuteReader(); + userDefinedTypes = reader.Cast().Select(r => (r.GetString(0), r.GetString(1))).ToArray(); + } + + if (userDefinedTypes.Any()) + { + var dropTypes = string.Concat(userDefinedTypes.Select(t => $"""DROP COLLATION "{t.Schema}"."{t.Name}" CASCADE;""")); + using var cmd = new GaussDBCommand(dropTypes, conn); + cmd.ExecuteNonQuery(); + } + } + + protected override string BuildCustomSql(DatabaseModel databaseModel) + // Some extensions create tables (e.g. PostGIS), so we must drop them first. + => databaseModel.GetPostgresExtensions() + .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) + .Aggregate( + new StringBuilder(), + (builder, s) => builder.Append("DROP EXTENSION ").Append(s).Append(";"), + builder => builder.ToString()); + + protected override string BuildCustomEndingSql(DatabaseModel databaseModel) + => databaseModel.GetPostgresEnums() + .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) + .Aggregate( + new StringBuilder(), + (builder, s) => builder.Append("DROP TYPE ").Append(s).Append(" CASCADE;"), + builder => builder.ToString()); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDatabaseFacadeExtensions.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDatabaseFacadeExtensions.cs new file mode 100644 index 0000000000..944a7ec538 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDatabaseFacadeExtensions.cs @@ -0,0 +1,8 @@ +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public static class GaussDBDatabaseFacadeExtensions +{ + public static void EnsureClean(this DatabaseFacade databaseFacade) + => databaseFacade.CreateExecutionStrategy() + .Execute(databaseFacade, database => new GaussDBDatabaseCleaner().Clean(database)); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDbContextOptionsBuilderExtensions.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000000..63e33395cd --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,15 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public static class GaussDBDbContextOptionsBuilderExtensions +{ + public static GaussDBDbContextOptionsBuilder ApplyConfiguration(this GaussDBDbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); + + optionsBuilder.CommandTimeout(GaussDBTestStore.CommandTimeout); + + return optionsBuilder; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBNorthwindTestStoreFactory.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBNorthwindTestStoreFactory.cs new file mode 100644 index 0000000000..99ff3efc53 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBNorthwindTestStoreFactory.cs @@ -0,0 +1,29 @@ +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class GaussDBNorthwindTestStoreFactory : GaussDBTestStoreFactory +{ + public const string Name = "Northwind"; + public static readonly string NorthwindConnectionString = GaussDBTestStore.CreateConnectionString(Name); + public static new GaussDBNorthwindTestStoreFactory Instance { get; } = new(); + + static GaussDBNorthwindTestStoreFactory() + { + // TODO: Switch to using GaussDBDataSource +#pragma warning disable CS0618 // Type or member is obsolete + GaussDBConnection.GlobalTypeMapper.EnableDynamicJson(); + GaussDBConnection.GlobalTypeMapper.EnableRecordsAsTuples(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + protected GaussDBNorthwindTestStoreFactory() + { + } + + public override TestStore GetOrCreate(string storeName) + => GaussDBTestStore.GetOrCreate( + 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.GaussDB.FunctionalTests/TestUtilities/GaussDBPrecompiledQueryTestHelpers.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBPrecompiledQueryTestHelpers.cs new file mode 100644 index 0000000000..9f894c912c --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBPrecompiledQueryTestHelpers.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class GaussDBPrecompiledQueryTestHelpers : PrecompiledQueryTestHelpers +{ + public static GaussDBPrecompiledQueryTestHelpers Instance = new(); + + protected override IEnumerable BuildProviderMetadataReferences() + { + yield return MetadataReference.CreateFromFile(typeof(GaussDBOptionsExtension).Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(GaussDBConnection).Assembly.Location); + yield return MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestHelpers.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestHelpers.cs new file mode 100644 index 0000000000..cd4dd978ec --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestHelpers.cs @@ -0,0 +1,18 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class GaussDBTestHelpers : RelationalTestHelpers +{ + protected GaussDBTestHelpers() { } + + public static GaussDBTestHelpers Instance { get; } = new(); + + public override IServiceCollection AddProviderServices(IServiceCollection services) + => services.AddEntityFrameworkGaussDB(); + + public override DbContextOptionsBuilder UseProviderOptions(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(new GaussDBConnection("Host=localhost;Database=DummyDatabase")); + + public override LoggingDefinitions LoggingDefinitions { get; } = new GaussDBLoggingDefinitions(); +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestStore.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestStore.cs new file mode 100644 index 0000000000..a394a10f97 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestStore.cs @@ -0,0 +1,441 @@ +using System.Data; +using System.Data.Common; +using System.Text.RegularExpressions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class GaussDBTestStore : RelationalTestStore +{ + private readonly string? _scriptPath; + private readonly string? _additionalSql; + + private const string Northwind = "Northwind"; + + public const int CommandTimeout = 600; + + public static readonly string NorthwindConnectionString = CreateConnectionString(Northwind); + + public static async Task GetNorthwindStoreAsync() + => (GaussDBTestStore)await GaussDBNorthwindTestStoreFactory.Instance + .GetOrCreate(GaussDBNorthwindTestStoreFactory.Name).InitializeAsync(null, (Func?)null); + + public static Task GetOrCreateInitializedAsync(string name) + => new GaussDBTestStore(name).InitializeGaussDBAsync(null, (Func?)null, null); + + public static GaussDBTestStore GetOrCreate( + string name, + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null) + => new(name, scriptPath, additionalSql, connectionStringOptions); + + public static GaussDBTestStore Create(string name, string? connectionStringOptions = null) + => new(name, connectionStringOptions: connectionStringOptions, shared: false); + + public static Task CreateInitializedAsync(string name) + => new GaussDBTestStore(name, shared: false).InitializeGaussDBAsync(null, (Func?)null, null); + + public GaussDBTestStore( + string name, + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null, + bool shared = true) + : base(name, shared, CreateConnection(name, connectionStringOptions)) + { + Name = name; + + if (scriptPath is not null) + { + // ReSharper disable once AssignNullToNotNullAttribute + _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(GaussDBTestStore).GetTypeInfo().Assembly.Location)!, scriptPath); + } + + _additionalSql = additionalSql; + } + + private static GaussDBConnection CreateConnection(string name, string? connectionStringOptions) + => new(CreateConnectionString(name, connectionStringOptions)); + + // ReSharper disable once MemberCanBePrivate.Global + public async Task InitializeGaussDBAsync( + IServiceProvider? serviceProvider, + Func? createContext, + Func? seed) + => (GaussDBTestStore)await InitializeAsync(serviceProvider, createContext, seed); + + // ReSharper disable once UnusedMember.Global + public async Task InitializeGaussDBAsync( + IServiceProvider serviceProvider, + Func createContext, + Func seed) + => await InitializeGaussDBAsync(serviceProvider, () => createContext(this), seed); + + protected override async Task InitializeAsync(Func createContext, Func? seed, Func? clean) + { + if (await CreateDatabaseAsync(clean)) + { + if (_scriptPath is not null) + { + ExecuteScript(_scriptPath); + + if (_additionalSql is not null) + { + Execute(Connection, command => command.ExecuteNonQuery(), _additionalSql); + } + } + else + { + await using var context = createContext(); + await context.Database.EnsureCreatedResilientlyAsync(); + + if (_additionalSql is not null) + { + Execute(Connection, command => command.ExecuteNonQuery(), _additionalSql); + } + + if (seed is not null) + { + await seed(context); + } + } + } + } + + public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) + { + 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 GaussDB + // sorts NULLs last by default. This configures the provider to emit NULLS FIRST. + .ReverseNullOrdering(); + + return UseConnectionString + ? builder.UseGaussDB(ConnectionString, npgsqlOptionsBuilder) + : builder.UseGaussDB(Connection, npgsqlOptionsBuilder); + } + + private async Task CreateDatabaseAsync(Func? clean) + { + await using var master = new GaussDBConnection(CreateAdminConnectionString()); + + if (await DatabaseExistsAsync(Name)) + { + if (_scriptPath is not null) + { + return false; + } + + 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((GaussDBConnection)Connection); + + return true; + } + + private static async Task WaitForExistsAsync(GaussDBConnection connection) + { + var retryCount = 0; + while (true) + { + try + { + if (connection.State != ConnectionState.Closed) + { + await connection.CloseAsync(); + } + + GaussDBConnection.ClearPool(connection); + + await connection.OpenAsync(); + await connection.CloseAsync(); + return; + } + catch (PostgresException e) + { + if (++retryCount >= 30 + || e.SqlState != "08001" && e.SqlState != "08000" && e.SqlState != "08006") + { + throw; + } + + await Task.Delay(100); + } + } + } + + // ReSharper disable once MemberCanBePrivate.Global + public void ExecuteScript(string scriptPath) + { + var script = File.ReadAllText(scriptPath); + Execute( + Connection, command => + { + foreach (var batch in + new Regex("^GO", RegexOptions.IgnoreCase | RegexOptions.Multiline, TimeSpan.FromMilliseconds(1000.0)) + .Split(script).Where(b => !string.IsNullOrEmpty(b))) + { + command.CommandText = batch; + command.ExecuteNonQuery(); + } + + return 0; + }, ""); + } + + private static string GetCreateDatabaseStatement(string name) + => $""" + CREATE DATABASE "{name}" + """; + + private static async Task DatabaseExistsAsync(string name) + { + await using var master = new GaussDBConnection(CreateAdminConnectionString()); + + return await ExecuteScalarAsync(master, $@"SELECT COUNT(*) FROM pg_database WHERE datname = '{name}'") > 0; + } + + public async Task DeleteDatabaseAsync() + { + if (!await DatabaseExistsAsync(Name)) + { + return; + } + + await using var master = new GaussDBConnection(CreateAdminConnectionString()); + + await ExecuteNonQueryAsync(master, GetDisconnectDatabaseSql(Name)); + await ExecuteNonQueryAsync(master, GetDropDatabaseSql(Name)); + + GaussDBConnection.ClearAllPools(); + } + + // Kill all connection to the database + private static string GetDisconnectDatabaseSql(string name) + => $""" +REVOKE CONNECT ON DATABASE "{name}" FROM PUBLIC; +SELECT pg_terminate_backend (pg_stat_activity.pid) + FROM pg_stat_activity + WHERE datname = '{name}' +"""; + + private static string GetDropDatabaseSql(string name) + => $""" + DROP DATABASE "{name}" + """; + + public override void OpenConnection() + => Connection.Open(); + + public override Task OpenConnectionAsync() + => Connection.OpenAsync(); + + // ReSharper disable once UnusedMember.Global + 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); + + // 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); + + // 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) + => 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) + => 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) + => Execute( + connection, command => + { + using var dataReader = command.ExecuteReader(); + + 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) + => ExecuteAsync( + connection, async command => + { + await using var dataReader = await command.ExecuteReaderAsync(); + + var results = Enumerable.Empty(); + while (await dataReader.ReadAsync()) + { + results = results.Concat([await dataReader.GetFieldValueAsync(0)]); + } + + return results; + }, sql, false, parameters); + + private static T Execute( + DbConnection connection, + Func execute, + string sql, + bool useTransaction = false, + object[]? parameters = null) + => ExecuteCommand(connection, execute, sql, useTransaction, parameters); + + private static T ExecuteCommand( + DbConnection connection, + Func execute, + string sql, + bool useTransaction, + object[]? parameters) + { + if (connection.State != ConnectionState.Closed) + { + connection.Close(); + } + + connection.Open(); + try + { + using var transaction = useTransaction ? connection.BeginTransaction() : null; + + T result; + using (var command = CreateCommand(connection, sql, parameters)) + { + command.Transaction = transaction; + result = execute(command); + } + + transaction?.Commit(); + + return result; + } + finally + { + if (connection.State == ConnectionState.Closed + && connection.State != ConnectionState.Closed) + { + connection.Close(); + } + } + } + + private static Task ExecuteAsync( + DbConnection connection, + Func> executeAsync, + string sql, + bool useTransaction = false, + IReadOnlyList? parameters = null) + => ExecuteCommandAsync(connection, executeAsync, sql, useTransaction, parameters); + + private static async Task ExecuteCommandAsync( + DbConnection connection, + Func> executeAsync, + string sql, + bool useTransaction, + IReadOnlyList? parameters) + { + if (connection.State != ConnectionState.Closed) + { + await connection.CloseAsync(); + } + + await connection.OpenAsync(); + try + { + await using var transaction = useTransaction ? await connection.BeginTransactionAsync() : null; + + T result; + await using (var command = CreateCommand(connection, sql, parameters)) + { + result = await executeAsync(command); + } + + if (transaction is not null) + { + await transaction.CommitAsync(); + } + + return result; + } + finally + { + if (connection.State == ConnectionState.Closed + && connection.State != ConnectionState.Closed) + { + await connection.CloseAsync(); + } + } + } + + private static DbCommand CreateCommand( + DbConnection connection, + string commandText, + IReadOnlyList? parameters = null) + { + var command = (GaussDBCommand)connection.CreateCommand(); + + command.CommandText = commandText; + command.CommandTimeout = CommandTimeout; + + if (parameters is not null) + { + for (var i = 0; i < parameters.Count; i++) + { + command.Parameters.AddWithValue("p" + i, parameters[i]); + } + } + + return command; + } + + public static string CreateConnectionString(string name, string? options = null) + { + var builder = new GaussDBConnectionStringBuilder(TestEnvironment.DefaultConnection) { Database = name }; + + if (options is not null) + { + builder.Options = options; + } + + return builder.ConnectionString; + } + + private static string CreateAdminConnectionString() + => CreateConnectionString("postgres"); + + public override Task CleanAsync(DbContext context) + { + context.Database.EnsureClean(); + return Task.CompletedTask; + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestStoreFactory.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestStoreFactory.cs new file mode 100644 index 0000000000..7b00cf8c17 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/GaussDBTestStoreFactory.cs @@ -0,0 +1,19 @@ +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class GaussDBTestStoreFactory( + string? scriptPath = null, + string? additionalSql = null, + string? connectionStringOptions = null, + bool useConnectionString = false) : RelationalTestStoreFactory +{ + public static GaussDBTestStoreFactory Instance { get; } = new(); + + public override TestStore Create(string storeName) + => new GaussDBTestStore(storeName, scriptPath, additionalSql, connectionStringOptions, shared: false) { UseConnectionString = useConnectionString }; + + public override TestStore GetOrCreate(string storeName) + => new GaussDBTestStore(storeName, scriptPath, additionalSql, connectionStringOptions, shared: true) { UseConnectionString = useConnectionString }; + + public override IServiceCollection AddProviderServices(IServiceCollection serviceCollection) + => serviceCollection.AddEntityFrameworkGaussDB(); +} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs similarity index 86% rename from test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs rename to test/EFCore.GaussDB.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs index 134331d743..4efacf2354 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/MinimumPostgresVersionAttribute.cs @@ -9,5 +9,5 @@ public ValueTask IsMetAsync() => new(TestEnvironment.PostgresVersion >= _version); public string SkipReason - => $"Requires PostgreSQL version {_version} or later."; + => $"Requires GaussDB version {_version} or later."; } diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/RequiresPostgisAttribute.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/RequiresPostgisAttribute.cs new file mode 100644 index 0000000000..ca905bf8e9 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/RequiresPostgisAttribute.cs @@ -0,0 +1,11 @@ +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public sealed class RequiresPostgisAttribute : Attribute, ITestCondition +{ + public ValueTask IsMetAsync() + => new(TestEnvironment.IsPostgisAvailable); + + public string SkipReason + => "Requires PostGIS"; +} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestEnvironment.cs similarity index 87% rename from test/EFCore.PG.FunctionalTests/TestUtilities/TestEnvironment.cs rename to test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestEnvironment.cs index 3887eb2ecc..c43fd6d8e1 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestEnvironment.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestEnvironment.cs @@ -16,7 +16,7 @@ static TestEnvironment() .AddEnvironmentVariables(); Config = configBuilder.Build() - .GetSection("Test:Npgsql"); + .GetSection("Test:GaussDB"); Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); } @@ -37,7 +37,7 @@ public static Version PostgresVersion return _postgresVersion; } - using var conn = new NpgsqlConnection(NpgsqlTestStore.CreateConnectionString("postgres")); + using var conn = new GaussDBConnection(GaussDBTestStore.CreateConnectionString("postgres")); conn.Open(); return _postgresVersion = conn.PostgreSqlVersion; } @@ -54,7 +54,7 @@ public static bool IsPostgisAvailable return _isPostgisAvailable.Value; } - using var conn = new NpgsqlConnection(NpgsqlTestStore.CreateConnectionString("postgres")); + using var conn = new GaussDBConnection(GaussDBTestStore.CreateConnectionString("postgres")); conn.Open(); using var cmd = conn.CreateCommand(); diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs similarity index 75% rename from test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs rename to test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs index 6b572da0ae..5053aa97d9 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestNpgsqlRetryingExecutionStrategy.cs @@ -1,35 +1,35 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL; +using HuaweiCloud.EntityFrameworkCore.GaussDB; namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class TestNpgsqlRetryingExecutionStrategy : NpgsqlRetryingExecutionStrategy +public class TestGaussDBRetryingExecutionStrategy : GaussDBRetryingExecutionStrategy { private const bool ErrorNumberDebugMode = false; private static readonly string[] AdditionalSqlStates = ["XX000"]; - public TestNpgsqlRetryingExecutionStrategy() + public TestGaussDBRetryingExecutionStrategy() : base( new DbContext( new DbContextOptionsBuilder() .EnableServiceProviderCaching(false) - .UseNpgsql(TestEnvironment.DefaultConnection).Options), + .UseGaussDB(TestEnvironment.DefaultConnection).Options), DefaultMaxRetryCount, DefaultMaxDelay, AdditionalSqlStates) { } - public TestNpgsqlRetryingExecutionStrategy(DbContext context) + public TestGaussDBRetryingExecutionStrategy(DbContext context) : base(context, DefaultMaxRetryCount, DefaultMaxDelay, AdditionalSqlStates) { } - public TestNpgsqlRetryingExecutionStrategy(DbContext context, TimeSpan maxDelay) + public TestGaussDBRetryingExecutionStrategy(DbContext context, TimeSpan maxDelay) : base(context, DefaultMaxRetryCount, maxDelay, AdditionalSqlStates) { } // ReSharper disable once UnusedMember.Global - public TestNpgsqlRetryingExecutionStrategy(ExecutionStrategyDependencies dependencies) + public TestGaussDBRetryingExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(dependencies, DefaultMaxRetryCount, DefaultMaxDelay, AdditionalSqlStates) { } diff --git a/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestPostgisConnection.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestPostgisConnection.cs new file mode 100644 index 0000000000..598d64d9a1 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestPostgisConnection.cs @@ -0,0 +1,51 @@ +using System.Data; +using System.Data.Common; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class TestPostgisConnection(RelationalConnectionDependencies dependencies, DbDataSource? dataSource = null) + : GaussDBRelationalConnection(dependencies, dataSource) +{ + public string ErrorCode { get; set; } = "XX000"; + public Queue OpenFailures { get; } = new(); + public int OpenCount { get; set; } + public Queue CommitFailures { get; } = new(); + public Queue ExecutionFailures { get; } = new(); + public int ExecutionCount { get; set; } + + public override bool Open(bool errorsExpected = false) + { + PreOpen(); + + return base.Open(errorsExpected); + } + + public override Task OpenAsync(CancellationToken cancellationToken, bool errorsExpected = false) + { + PreOpen(); + + return base.OpenAsync(cancellationToken, errorsExpected); + } + + private void PreOpen() + { + if (DbConnection.State == ConnectionState.Open) + { + return; + } + + OpenCount++; + if (OpenFailures.Count <= 0) + { + return; + } + + var fail = OpenFailures.Dequeue(); + + if (fail.HasValue) + { + throw new PostgresException("Simulated failure", "ERROR", "ERROR", ErrorCode); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs rename to test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs index 3a9732e376..dfe34a7221 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestRelationalCommandBuilderFactory.cs @@ -206,7 +206,7 @@ public DbCommand CreateDbCommand(RelationalCommandParameterObject parameterObjec private string? PreExecution(IRelationalConnection connection) { string? errorNumber = null; - var testConnection = (TestNpgsqlConnection)connection; + var testConnection = (TestPostgisConnection)connection; testConnection.ExecutionCount++; if (testConnection.ExecutionFailures.Count > 0) diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestRelationalTransaction.cs similarity index 96% rename from test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs rename to test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestRelationalTransaction.cs index b3d1b74d1c..05ae0fdd6a 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestRelationalTransaction.cs +++ b/test/EFCore.GaussDB.FunctionalTests/TestUtilities/TestRelationalTransaction.cs @@ -23,7 +23,7 @@ public class TestRelationalTransaction( ISqlGenerationHelper sqlGenerationHelper) : RelationalTransaction(connection, transaction, new Guid(), logger, transactionOwned, sqlGenerationHelper) { - private readonly TestNpgsqlConnection _testConnection = (TestNpgsqlConnection)connection; + private readonly TestPostgisConnection _testConnection = (TestPostgisConnection)connection; public override void Commit() { diff --git a/test/EFCore.GaussDB.FunctionalTests/TransactionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/TransactionGaussDBTest.cs new file mode 100644 index 0000000000..8dd6635cb4 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TransactionGaussDBTest.cs @@ -0,0 +1,108 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class TransactionGaussDBTest(TransactionGaussDBTest.TransactionGaussDBFixture fixture) + : TransactionTestBase(fixture) +{ + public override Task SaveChanges_can_be_used_with_AutoTransactionBehavior_Never(bool async) + // GaussDB batches the inserts, creating an implicit transaction which fails the test + // (see https://github.com/npgsql/npgsql/issues/1307) + => Task.CompletedTask; + +#pragma warning disable CS0618 // AutoTransactionsEnabled is obsolete + public override Task SaveChanges_can_be_used_with_AutoTransactionsEnabled_false(bool async) + // GaussDB batches the inserts, creating an implicit transaction which fails the test + // (see https://github.com/npgsql/npgsql/issues/1307) + => Task.CompletedTask; +#pragma warning restore CS0618 + + protected override DbContext CreateContextWithConnectionString() + { + var options = Fixture.AddOptions( + new DbContextOptionsBuilder() + .UseGaussDB( + TestStore.ConnectionString, + b => b.ApplyConfiguration() + .ExecutionStrategy(c => new GaussDBExecutionStrategy(c)) + .ReverseNullOrdering())) + .UseInternalServiceProvider(Fixture.ServiceProvider); + + return new DbContext(options.Options); + } + + // In GaussDB, once the transaction enters the failed state it is always rolled back completely, + // so none of the inserts are left. + public override async Task SaveChanges_can_be_used_with_no_savepoint(bool async) + { + await using (var context = CreateContext()) + { + context.Database.AutoSavepointsEnabled = false; + + await using var transaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction(); + + context.Add(new TransactionCustomer { Id = 77, Name = "Bobble" }); + + if (async) + { + await context.SaveChangesAsync(); + } + else + { + context.SaveChanges(); + } + + context.Add(new TransactionCustomer { Id = 78, Name = "Hobble" }); + context.Add(new TransactionCustomer { Id = 1, Name = "Gobble" }); // Cause SaveChanges failure + + if (async) + { + await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + await transaction.CommitAsync(); + } + else + { + Assert.Throws(() => context.SaveChanges()); + transaction.Commit(); + } + + context.Database.AutoSavepointsEnabled = true; + } + + await using (var context = CreateContext()) + { + Assert.Equal(2, context.Set().Max(c => c.Id)); + } + } + + // Test generates an exception (by double-releasing the savepoint), which causes the transaction to enter + // a failed state and roll back all changes. + public override Task Savepoint_can_be_released(bool async) + => Task.CompletedTask; + + protected override bool AmbientTransactionsSupported + => true; + + protected override bool SnapshotSupported + => true; + + protected override bool DirtyReadsOccur + => false; + + public class TransactionGaussDBFixture : TransactionFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new GaussDBDbContextOptionsBuilder( + base.AddOptions(builder)) + .ExecutionStrategy(c => new GaussDBExecutionStrategy(c)); + return builder; + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TransactionInterceptionGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/TransactionInterceptionGaussDBTest.cs new file mode 100644 index 0000000000..c0698394be --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TransactionInterceptionGaussDBTest.cs @@ -0,0 +1,41 @@ +namespace Microsoft.EntityFrameworkCore; + +public abstract class TransactionInterceptionGaussDBTestBase(TransactionInterceptionGaussDBTestBase.InterceptionGaussDBFixtureBase fixture) + : TransactionInterceptionTestBase(fixture) +{ + public abstract class InterceptionGaussDBFixtureBase : InterceptionFixtureBase + { + protected override string StoreName + => "TransactionInterception"; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkGaussDB(), injectedInterceptors); + } + + public class TransactionInterceptionGaussDBTest(TransactionInterceptionGaussDBTest.InterceptionGaussDBFixture fixture) + : TransactionInterceptionGaussDBTestBase(fixture), IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener + => false; + } + } + + public class TransactionInterceptionWithDiagnosticsGaussDBTest( + TransactionInterceptionWithDiagnosticsGaussDBTest.InterceptionGaussDBFixture fixture) + : TransactionInterceptionGaussDBTestBase(fixture), + IClassFixture + { + public class InterceptionGaussDBFixture : InterceptionGaussDBFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener + => true; + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/TwoDatabasesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/TwoDatabasesGaussDBTest.cs new file mode 100644 index 0000000000..40656a22aa --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/TwoDatabasesGaussDBTest.cs @@ -0,0 +1,22 @@ +namespace Microsoft.EntityFrameworkCore; + +public class TwoDatabasesGaussDBTest(GaussDBFixture fixture) : TwoDatabasesTestBase(fixture), IClassFixture +{ + protected new GaussDBFixture Fixture + => (GaussDBFixture)base.Fixture; + + protected override DbContextOptionsBuilder CreateTestOptions( + DbContextOptionsBuilder optionsBuilder, + bool withConnectionString = false, + bool withNullConnectionString = false) + => withConnectionString + ? withNullConnectionString + ? optionsBuilder.UseGaussDB((string?)null) + : optionsBuilder.UseGaussDB(DummyConnectionString) + : optionsBuilder.UseGaussDB(); + + protected override TwoDatabasesWithDataContext CreateBackingContext(string databaseName) + => new(Fixture.CreateOptions(GaussDBTestStore.Create(databaseName))); + + protected override string DummyConnectionString { get; } = "Host=localhost;Database=DoesNotExist"; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Update/JsonUpdateGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Update/JsonUpdateGaussDBTest.cs new file mode 100644 index 0000000000..9437bb3cc0 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Update/JsonUpdateGaussDBTest.cs @@ -0,0 +1,2146 @@ +using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Update; + +public class JsonUpdateGaussDBTest : JsonUpdateTestBase +{ + public JsonUpdateGaussDBTest(JsonUpdateGaussDBFixture fixture) + : base(fixture) + { + ClearLog(); + } + + public override async Task Add_element_to_json_collection_branch() + { + await base.Add_element_to_json_collection_branch(); + + AssertSql( + """ +@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) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Add_element_to_json_collection_leaf() + { + await base.Add_element_to_json_collection_leaf(); + + AssertSql( + """ +@p0='[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"},{"SomethingSomething":"ss1"}]' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch,OwnedCollectionLeaf}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Add_element_to_json_collection_on_derived() + { + await base.Add_element_to_json_collection_on_derived(); + + AssertSql( + """ +@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 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Discriminator", j."Name", j."Fraction", j."CollectionOnBase", j."ReferenceOnBase", j."CollectionOnDerived", j."ReferenceOnDerived" +FROM "JsonEntitiesInheritance" AS j +WHERE j."Discriminator" = 'JsonEntityInheritanceDerived' +LIMIT 2 +"""); + } + + public override async Task Add_element_to_json_collection_root() + { + await base.Add_element_to_json_collection_root(); + + AssertSql( + """ +@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 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Add_element_to_json_collection_root_null_navigations() + { + await base.Add_element_to_json_collection_root_null_navigations(); + + AssertSql( + """ +@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 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Add_entity_with_json() + { + await base.Add_entity_with_json(); + + AssertSql( + """ +@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) +@p4='NewEntity' + +INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "OwnedCollectionRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3, @p4); +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Add_entity_with_json_null_navigations() + { + await base.Add_entity_with_json_null_navigations(); + + AssertSql( + """ +@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' + +INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "Id", "EntityBasicId", "Name") +VALUES (@p0, @p1, @p2, @p3); +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Add_json_reference_leaf() + { + await base.Add_json_reference_leaf(); + + AssertSql( + """ +@p0='{"SomethingSomething":"ss3"}' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch,0,OwnedReferenceLeaf}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Add_json_reference_root() + { + await base.Add_json_reference_root(); + + AssertSql( + """ +@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 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Delete_entity_with_json() + { + await base.Delete_entity_with_json(); + + AssertSql( + """ +@p0='1' + +DELETE FROM "JsonEntitiesBasic" +WHERE "Id" = @p0; +""", + // + """ +SELECT count(*)::int +FROM "JsonEntitiesBasic" AS j +"""); + } + + public override async Task Delete_json_collection_branch() + { + await base.Delete_json_collection_branch(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Delete_json_collection_root() + { + await base.Delete_json_collection_root(); + + AssertSql( + """ +@p0=NULL (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Delete_json_reference_leaf() + { + await base.Delete_json_reference_leaf(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch,OwnedReferenceLeaf}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Delete_json_reference_root() + { + await base.Delete_json_reference_root(); + + AssertSql( + """ +@p0=NULL (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = @p0 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_element_in_json_collection_branch() + { + await base.Edit_element_in_json_collection_branch(); + + AssertSql( + """ +@p0='"2111-11-11T00:00:00"' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{0,OwnedCollectionBranch,0,Date}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_element_in_json_collection_root1() + { + await base.Edit_element_in_json_collection_root1(); + + AssertSql( + """ +@p0='"Modified"' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{0,Name}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_element_in_json_collection_root2() + { + await base.Edit_element_in_json_collection_root2(); + + AssertSql( + """ +@p0='"Modified"' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,Name}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_element_in_json_multiple_levels_partial_update() + { + await base.Edit_element_in_json_multiple_levels_partial_update(); + + AssertSql( + """ +@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 +WHERE "Id" = @p2; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection() + { + await base.Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection(); + + AssertSql( + """ +@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) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_two_elements_in_the_same_json_collection() + { + await base.Edit_two_elements_in_the_same_json_collection(); + + AssertSql( + """ +@p0='[{"SomethingSomething":"edit1"},{"SomethingSomething":"edit2"}]' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_two_elements_in_the_same_json_collection_at_the_root() + { + await base.Edit_two_elements_in_the_same_json_collection_at_the_root(); + + AssertSql( + """ +@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 +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_collection_element_and_reference_at_once() + { + await base.Edit_collection_element_and_reference_at_once(); + + AssertSql( + """ +@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) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_single_enum_property() + { + await base.Edit_single_enum_property(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_single_numeric_property() + { + await base.Edit_single_numeric_property(); + + AssertSql( + """ +@p0='1024' (Nullable = false) (DbType = Object) +@p1='999' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,Number}', @p0), "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{Number}', @p1) +WHERE "Id" = @p2; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_bool() + { + await base.Edit_single_property_bool(); + + AssertSql( + """ +@p0='true' (Nullable = false) (DbType = Object) +@p1='false' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestBoolean}', @p0), "Reference" = jsonb_set("Reference", '{TestBoolean}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_byte() + { + await base.Edit_single_property_byte(); + + AssertSql( + """ +@p0='14' (Nullable = false) (DbType = Object) +@p1='25' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestByte}', @p0), "Reference" = jsonb_set("Reference", '{TestByte}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_char() + { + await base.Edit_single_property_char(); + + AssertSql( + """ +@p0='"t"' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesAllTypes" SET "Reference" = jsonb_set("Reference", '{TestCharacter}', @p0) +WHERE "Id" = @p1; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_datetime() + { + await base.Edit_single_property_datetime(); + + AssertSql( + """ +@p0='"3000-01-01T12:34:56"' (Nullable = false) (DbType = Object) +@p1='"3000-01-01T12:34:56"' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTime}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTime}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_datetimeoffset() + { + await base.Edit_single_property_datetimeoffset(); + + AssertSql( + """ +@p0='"3000-01-01T12:34:56-04:00"' (Nullable = false) (DbType = Object) +@p1='"3000-01-01T12:34:56-04:00"' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTimeOffset}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTimeOffset}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_decimal() + { + await base.Edit_single_property_decimal(); + + AssertSql( + """ +@p0='-13579.01' (Nullable = false) (DbType = Object) +@p1='-13579.01' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDecimal}', @p0), "Reference" = jsonb_set("Reference", '{TestDecimal}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_double() + { + await base.Edit_single_property_double(); + + AssertSql( + """ +@p0='-1.23579' (Nullable = false) (DbType = Object) +@p1='-1.23579' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDouble}', @p0), "Reference" = jsonb_set("Reference", '{TestDouble}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_guid() + { + await base.Edit_single_property_guid(); + + AssertSql( + """ +@p0='"12345678-1234-4321-5555-987654321000"' (Nullable = false) (DbType = Object) +@p1='"12345678-1234-4321-5555-987654321000"' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestGuid}', @p0), "Reference" = jsonb_set("Reference", '{TestGuid}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_int16() + { + await base.Edit_single_property_int16(); + + AssertSql( + """ +@p0='-3234' (Nullable = false) (DbType = Object) +@p1='-3234' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt16}', @p0), "Reference" = jsonb_set("Reference", '{TestInt16}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_int32() + { + await base.Edit_single_property_int32(); + + AssertSql( + """ +@p0='-3234' (Nullable = false) (DbType = Object) +@p1='-3234' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestInt32}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_int64() + { + await base.Edit_single_property_int64(); + + AssertSql( + """ +@p0='-3234' (Nullable = false) (DbType = Object) +@p1='-3234' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt64}', @p0), "Reference" = jsonb_set("Reference", '{TestInt64}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_signed_byte() + { + await base.Edit_single_property_signed_byte(); + + AssertSql( + """ +@p0='-108' (Nullable = false) (DbType = Object) +@p1='-108' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSignedByte}', @p0), "Reference" = jsonb_set("Reference", '{TestSignedByte}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_single() + { + await base.Edit_single_property_single(); + + AssertSql( + """ +@p0='-7.234' (Nullable = false) (DbType = Object) +@p1='-7.234' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSingle}', @p0), "Reference" = jsonb_set("Reference", '{TestSingle}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_timespan() + { + await base.Edit_single_property_timespan(); + + AssertSql( + """ +@p0='"10:01:01.007"' (Nullable = false) (DbType = Object) +@p1='"10:01:01.007"' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeSpan}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeSpan}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_uint16() + { + await base.Edit_single_property_uint16(); + + AssertSql( + """ +@p0='1534' (Nullable = false) (DbType = Object) +@p1='1534' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt16}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt16}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_uint32() + { + await base.Edit_single_property_uint32(); + + AssertSql( + """ +@p0='1237775789' (Nullable = false) (DbType = Object) +@p1='1237775789' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt32}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_uint64() + { + await base.Edit_single_property_uint64(); + + AssertSql( + """ +@p0='1234555555123456789' (Nullable = false) (DbType = Object) +@p1='1234555555123456789' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt64}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt64}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_int32() + { + await base.Edit_single_property_nullable_int32(); + + AssertSql( + """ +@p0='122354' (Nullable = false) (DbType = Object) +@p1='64528' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_int32_set_to_null() + { + await base.Edit_single_property_nullable_int32_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_enum() + { + await base.Edit_single_property_enum(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_enum_with_int_converter() + { + await base.Edit_single_property_enum_with_int_converter(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_enum() + { + await base.Edit_single_property_nullable_enum(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_enum_set_to_null() + { + await base.Edit_single_property_nullable_enum_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnum}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnum}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_enum_with_int_converter() + { + await base.Edit_single_property_nullable_enum_with_int_converter(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_enum_with_int_converter_set_to_null() + { + await base.Edit_single_property_nullable_enum_with_int_converter_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithIntConverter}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithIntConverter}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_enum_with_converter_that_handles_nulls() + { + await base.Edit_single_property_nullable_enum_with_converter_that_handles_nulls(); + + AssertSql( + """ +@p0='"Three"' (Nullable = false) (DbType = Object) +@p1='"One"' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithConverterThatHandlesNulls}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithConverterThatHandlesNulls}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_nullable_enum_with_converter_that_handles_nulls_set_to_null() + { + await base.Edit_single_property_nullable_enum_with_converter_that_handles_nulls_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithConverterThatHandlesNulls}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithConverterThatHandlesNulls}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_two_properties_on_same_entity_updates_the_entire_entity() + { + await base.Edit_two_properties_on_same_entity_updates_the_entire_entity(); + + AssertSql( + """ +@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,9223372036854775807]}' (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,9223372036854775807]}' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0}', @p0), "Reference" = @p1 +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_a_scalar_property_and_reference_navigation_on_the_same_entity() + { + await base.Edit_a_scalar_property_and_reference_navigation_on_the_same_entity(); + + AssertSql( + """ +@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) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_a_scalar_property_and_collection_navigation_on_the_same_entity() + { + await base.Edit_a_scalar_property_and_collection_navigation_on_the_same_entity(); + + AssertSql( + """ +@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) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_a_scalar_property_and_another_property_behind_reference_navigation_on_the_same_entity() + { + await base.Edit_a_scalar_property_and_another_property_behind_reference_navigation_on_the_same_entity(); + + AssertSql( + """ +@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) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_with_converter_bool_to_int_zero_one() + { + await base.Edit_single_property_with_converter_bool_to_int_zero_one(); + + AssertSql( + """ +@p0='0' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{BoolConvertedToIntZeroOne}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE j."Id" = 1 +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_with_converter_bool_to_string_True_False() + { + await base.Edit_single_property_with_converter_bool_to_string_True_False(); + + AssertSql( + """ +@p0='"True"' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{BoolConvertedToStringTrueFalse}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE j."Id" = 1 +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_with_converter_bool_to_string_Y_N() + { + await base.Edit_single_property_with_converter_bool_to_string_Y_N(); + + AssertSql( + """ +@p0='"N"' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{BoolConvertedToStringYN}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE j."Id" = 1 +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_with_converter_int_zero_one_to_bool() + { + await base.Edit_single_property_with_converter_int_zero_one_to_bool(); + + AssertSql( + """ +@p0='true' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{IntZeroOneConvertedToBool}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE j."Id" = 1 +LIMIT 2 +"""); + } + + [ConditionalFact] + public override async Task Edit_single_property_with_converter_string_True_False_to_bool() + { + await base.Edit_single_property_with_converter_string_True_False_to_bool(); + + AssertSql( + """ +@p0='false' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{StringTrueFalseConvertedToBool}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE j."Id" = 1 +LIMIT 2 +"""); + } + + [ConditionalFact] + public override async Task Edit_single_property_with_converter_string_Y_N_to_bool() + { + await base.Edit_single_property_with_converter_string_Y_N_to_bool(); + + AssertSql( + """ +@p0='true' (Nullable = false) (DbType = Object) +@p1='1' + +UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{StringYNConvertedToBool}', @p0) +WHERE "Id" = @p1; +""", + // + """ +SELECT j."Id", j."Reference" +FROM "JsonEntitiesConverters" AS j +WHERE j."Id" = 1 +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_collection_of_numeric() + { + await base.Edit_single_property_collection_of_numeric(); + + AssertSql( + """ +@p0='[1024,2048]' (Nullable = false) (DbType = Object) +@p1='[999,997]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,Numbers}', @p0), "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{Numbers}', @p1) +WHERE "Id" = @p2; +""", + // + """ +SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS j +LIMIT 2 +"""); + } + + public override async Task Edit_single_property_collection_of_bool() + { + await base.Edit_single_property_collection_of_bool(); + + AssertSql( + """ +@p0='[true,true,true,false]' (Nullable = false) (DbType = Object) +@p1='[true,true,false]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestBooleanCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestBooleanCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_byte() + { + await base.Edit_single_property_collection_of_byte(); + + AssertSql( + """ +@p0='"Dg=="' (Nullable = false) (DbType = Object) +@p1='"GRo="' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestByteCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestByteCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_char() + { + // GaussDB does not support the 0 char in text + var exception = await Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_char()); + var pgException = Assert.IsType(exception.InnerException); + Assert.Equal("22P05", pgException.SqlState); // untranslatable_character + } + + public override async Task Edit_single_property_collection_of_datetime() + { + await base.Edit_single_property_collection_of_datetime(); + + AssertSql( + """ +@p0='["2000-01-01T12:34:56","3000-01-01T12:34:56","3000-01-01T12:34:56"]' (Nullable = false) (DbType = Object) +@p1='["2000-01-01T12:34:56","3000-01-01T12:34:56","3000-01-01T12:34:56"]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTimeCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTimeCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_datetimeoffset() + { + await base.Edit_single_property_collection_of_datetimeoffset(); + + AssertSql( + """ +@p0='["3000-01-01T12:34:56-04:00"]' (Nullable = false) (DbType = Object) +@p1='["3000-01-01T12:34:56-04:00"]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTimeOffsetCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTimeOffsetCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_decimal() + { + await base.Edit_single_property_collection_of_decimal(); + + AssertSql( + """ +@p0='[-13579.01]' (Nullable = false) (DbType = Object) +@p1='[-13579.01]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDecimalCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDecimalCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_double() + { + await base.Edit_single_property_collection_of_double(); + + AssertSql( + """ +@p0='[-1.23456789,1.23456789,0,-1.23579]' (Nullable = false) (DbType = Object) +@p1='[-1.23456789,1.23456789,0,-1.23579]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDoubleCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDoubleCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_guid() + { + await base.Edit_single_property_collection_of_guid(); + + AssertSql( + """ +@p0='["12345678-1234-4321-5555-987654321000"]' (Nullable = false) (DbType = Object) +@p1='["12345678-1234-4321-5555-987654321000"]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestGuidCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestGuidCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_int16() + { + await base.Edit_single_property_collection_of_int16(); + + AssertSql( + """ +@p0='[-3234]' (Nullable = false) (DbType = Object) +@p1='[-3234]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt16Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestInt16Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_int32() + { + await base.Edit_single_property_collection_of_int32(); + + AssertSql( + """ +@p0='[-3234]' (Nullable = false) (DbType = Object) +@p1='[-3234]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestInt32Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_int64() + { + await base.Edit_single_property_collection_of_int64(); + + AssertSql( + """ +@p0='[]' (Nullable = false) (DbType = Object) +@p1='[]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt64Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestInt64Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_signed_byte() + { + await base.Edit_single_property_collection_of_signed_byte(); + + AssertSql( + """ +@p0='[-108]' (Nullable = false) (DbType = Object) +@p1='[-108]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSignedByteCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestSignedByteCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_single() + { + await base.Edit_single_property_collection_of_single(); + + AssertSql( + """ +@p0='[-1.234,-1.234]' (Nullable = false) (DbType = Object) +@p1='[0,-1.234]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSingleCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestSingleCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_timespan() + { + await base.Edit_single_property_collection_of_timespan(); + + AssertSql( + """ +@p0='["10:09:08.007","10:01:01.007"]' (Nullable = false) (DbType = Object) +@p1='["10:01:01.007","-09:50:51.993"]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeSpanCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeSpanCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_dateonly() + { + await base.Edit_single_property_collection_of_dateonly(); + + AssertSql( + """ +@p0='["3234-01-23","0001-01-07"]' (Nullable = false) (DbType = Object) +@p1='["0001-01-07","4321-01-21"]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateOnlyCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDateOnlyCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_timeonly() + { + await base.Edit_single_property_collection_of_timeonly(); + + AssertSql( + """ +@p0='["13:42:23.0000000","01:01:07.0000000"]' (Nullable = false) (DbType = Object) +@p1='["01:01:07.0000000","07:17:27.0000000"]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeOnlyCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeOnlyCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_uint16() + { + await base.Edit_single_property_collection_of_uint16(); + + AssertSql( + """ +@p0='[1534]' (Nullable = false) (DbType = Object) +@p1='[1534]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt16Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt16Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_uint32() + { + await base.Edit_single_property_collection_of_uint32(); + + AssertSql( + """ +@p0='[1237775789]' (Nullable = false) (DbType = Object) +@p1='[1237775789]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt32Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_uint64() + { + await base.Edit_single_property_collection_of_uint64(); + + AssertSql( + """ +@p0='[1234555555123456789]' (Nullable = false) (DbType = Object) +@p1='[1234555555123456789]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt64Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt64Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_int32() + { + await base.Edit_single_property_collection_of_nullable_int32(); + + AssertSql( + """ +@p0='[null,77]' (Nullable = false) (DbType = Object) +@p1='[null,-2147483648,0,null,2147483647,null,77,null]' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_int32_set_to_null() + { + await base.Edit_single_property_collection_of_nullable_int32_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32Collection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_enum() + { + await base.Edit_single_property_collection_of_enum(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_enum_with_int_converter() + { + await base.Edit_single_property_collection_of_enum_with_int_converter(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_enum() + { + await base.Edit_single_property_collection_of_nullable_enum(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_enum_set_to_null() + { + await base.Edit_single_property_collection_of_nullable_enum_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_enum_with_int_converter() + { + await base.Edit_single_property_collection_of_nullable_enum_with_int_converter(); + + AssertSql( + """ +@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) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_enum_with_int_converter_set_to_null() + { + await base.Edit_single_property_collection_of_nullable_enum_with_int_converter_set_to_null(); + + AssertSql( + """ +@p0='null' (Nullable = false) (DbType = Object) +@p1='null' (Nullable = false) (DbType = Object) +@p2='1' + +UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithIntConverterCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithIntConverterCollection}', @p1) +WHERE "Id" = @p2; +""", + // + """ +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 +"""); + } + + public override async Task Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls_set_to_null() + { + await base.Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls_set_to_null(); + + 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 j."Id" = 1 +LIMIT 2 +"""); + } + + // https://github.com/dotnet/efcore/pull/31831/files#r1393411950 + public override Task Add_and_update_top_level_optional_owned_collection_to_JSON(bool? value) + => Assert.ThrowsAsync(() => base.Add_and_update_top_level_optional_owned_collection_to_JSON(value)); + + public override async Task Add_and_update_nested_optional_primitive_collection(bool? value) + { + // GaussDB does not support the 0 char in text + var exception = await Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_char()); + var pgException = Assert.IsType(exception.InnerException); + Assert.Equal("22P05", pgException.SqlState); // untranslatable_character + } + + #region Skipped tests because of unsupported list type outside of JSON + + // The following tests fail because the properties they access are ignored in the model (see OnModelCreating below). + // We do not yet support arbitrary list types outside of JSON. + public override Task Edit_single_property_relational_collection_of_bool() + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_bool()); + + public override Task Edit_single_property_relational_collection_of_byte() + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_byte()); + + public override Task Edit_single_property_relational_collection_of_char() + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_char()); + + public override Task Edit_single_property_relational_collection_of_datetimeoffset() + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_datetimeoffset()); + + public override Task Edit_single_property_relational_collection_of_double() + => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_double()); + + public override Task Edit_single_property_relational_collection_of_enum() + => Assert.ThrowsAsync(() => base.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()); + + 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( + () => 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()); + + public override Task Edit_single_property_relational_collection_of_uint16() + => Assert.ThrowsAsync(() => base.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(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class JsonUpdateGaussDBFixture : JsonUpdateFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + base.ConfigureConventions(configurationBuilder); + + // The tests seed Unspecified DateTimes, but our default mapping for DateTime is timestamptz, which requires UTC. + // Map these properties to "timestamp without time zone". + configurationBuilder.Properties().HaveColumnType("timestamp without time zone"); + configurationBuilder.Properties>().HaveColumnType("timestamp without time zone[]"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // The following are ignored since we do not support mapping IList (as opposed to array/List) on regular properties + // (since that's not supported at the ADO.NET layer). However, we do support IList inside JSON documents, since that doesn't + // rely on ADO.NET support. + modelBuilder.Entity( + b => + { + b.Ignore(j => j.TestEnumCollection); + b.Ignore(j => j.TestUnsignedInt16Collection); + b.Ignore(j => j.TestNullableEnumCollection); + b.Ignore(j => j.TestNullableEnumWithIntConverterCollection); + b.Ignore(j => j.TestCharacterCollection); + b.Ignore(j => j.TestNullableInt32Collection); + b.Ignore(j => j.TestUnsignedInt64Collection); + + b.Ignore(j => j.TestByteCollection); + b.Ignore(j => j.TestBooleanCollection); + b.Ignore(j => j.TestDateTimeOffsetCollection); + b.Ignore(j => j.TestDoubleCollection); + b.Ignore(j => j.TestInt16Collection); + }); + + // These use collection types which are unsupported for arrays at the GaussDB 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 GaussDB (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.GaussDB.FunctionalTests/Update/NonSharedModelUpdatesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Update/NonSharedModelUpdatesGaussDBTest.cs new file mode 100644 index 0000000000..5079f418a8 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Update/NonSharedModelUpdatesGaussDBTest.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.Update; + +public class NonSharedModelUpdatesGaussDBTest(NonSharedFixture fixture) : NonSharedModelUpdatesTestBase(fixture) +{ + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Update/StoreValueGenerationGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Update/StoreValueGenerationGaussDBTest.cs new file mode 100644 index 0000000000..19c12231f2 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Update/StoreValueGenerationGaussDBTest.cs @@ -0,0 +1,417 @@ +using System.Text; +using Microsoft.EntityFrameworkCore.TestModels.StoreValueGenerationModel; + +namespace Microsoft.EntityFrameworkCore.Update; + +public class StoreValueGenerationGaussDBTest : StoreValueGenerationTestBase< + StoreValueGenerationGaussDBTest.StoreValueGenerationGaussDBFixture> +{ + public StoreValueGenerationGaussDBTest(StoreValueGenerationGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override bool ShouldCreateImplicitTransaction( + EntityState firstOperationType, + EntityState? secondOperationType, + GeneratedValues generatedValues, + bool withSameEntityType) + => secondOperationType is not null; + + protected override int ShouldExecuteInNumberOfCommands( + EntityState firstOperationType, + EntityState? secondOperationType, + GeneratedValues generatedValues, + bool withDatabaseGenerated) + => 1; + + #region Single operation + + public override async Task Add_with_generated_values(bool async) + { + await base.Add_with_generated_values(async); + + AssertSql( + """ +@p0='1000' + +INSERT INTO "WithSomeDatabaseGenerated" ("Data2") +VALUES (@p0) +RETURNING "Id", "Data1"; +"""); + } + + public override async Task Add_with_no_generated_values(bool async) + { + await base.Add_with_no_generated_values(async); + + AssertSql( + """ +@p0='100' +@p1='1000' +@p2='1000' + +INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") +VALUES (@p0, @p1, @p2); +"""); + } + + public override async Task Add_with_all_generated_values(bool async) + { + await base.Add_with_all_generated_values(async); + + AssertSql( + """ +INSERT INTO "WithAllDatabaseGenerated" +DEFAULT VALUES +RETURNING "Id", "Data1", "Data2"; +"""); + } + + public override async Task Modify_with_generated_values(bool async) + { + await base.Modify_with_generated_values(async); + + AssertSql( + """ +@p1='1' +@p0='1000' + +UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p0 +WHERE "Id" = @p1 +RETURNING "Data1"; +"""); + } + + public override async Task Modify_with_no_generated_values(bool async) + { + await base.Modify_with_no_generated_values(async); + + AssertSql( + """ +@p2='1' +@p0='1000' +@p1='1000' + +UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p0, "Data2" = @p1 +WHERE "Id" = @p2; +"""); + } + + public override async Task Delete(bool async) + { + await base.Delete(async); + + AssertSql( + """ +@p0='1' + +DELETE FROM "WithSomeDatabaseGenerated" +WHERE "Id" = @p0; +"""); + } + + #endregion Single operation + + #region Two operations with same entity type + + public override async Task Add_Add_with_same_entity_type_and_generated_values(bool async) + { + await base.Add_Add_with_same_entity_type_and_generated_values(async); + + AssertSql( + """ +@p0='1000' +@p1='1001' + +INSERT INTO "WithSomeDatabaseGenerated" ("Data2") +VALUES (@p0) +RETURNING "Id", "Data1"; +INSERT INTO "WithSomeDatabaseGenerated" ("Data2") +VALUES (@p1) +RETURNING "Id", "Data1"; +"""); + } + + public override async Task Add_Add_with_same_entity_type_and_no_generated_values(bool async) + { + await base.Add_Add_with_same_entity_type_and_no_generated_values(async); + + AssertSql( + """ +@p0='100' +@p1='1000' +@p2='1000' +@p3='101' +@p4='1001' +@p5='1001' + +INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") +VALUES (@p0, @p1, @p2); +INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") +VALUES (@p3, @p4, @p5); +"""); + } + + public override async Task Add_Add_with_same_entity_type_and_all_generated_values(bool async) + { + await base.Add_Add_with_same_entity_type_and_all_generated_values(async); + + AssertSql( + """ +INSERT INTO "WithAllDatabaseGenerated" +DEFAULT VALUES +RETURNING "Id", "Data1", "Data2"; +INSERT INTO "WithAllDatabaseGenerated" +DEFAULT VALUES +RETURNING "Id", "Data1", "Data2"; +"""); + } + + public override async Task Modify_Modify_with_same_entity_type_and_generated_values(bool async) + { + await base.Modify_Modify_with_same_entity_type_and_generated_values(async); + + AssertSql( + """ +@p1='1' +@p0='1000' +@p3='2' +@p2='1001' + +UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p0 +WHERE "Id" = @p1 +RETURNING "Data1"; +UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p2 +WHERE "Id" = @p3 +RETURNING "Data1"; +"""); + } + + public override async Task Modify_Modify_with_same_entity_type_and_no_generated_values(bool async) + { + await base.Modify_Modify_with_same_entity_type_and_no_generated_values(async); + + AssertSql( + """ +@p2='1' +@p0='1000' +@p1='1000' +@p5='2' +@p3='1001' +@p4='1001' + +UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p0, "Data2" = @p1 +WHERE "Id" = @p2; +UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p3, "Data2" = @p4 +WHERE "Id" = @p5; +"""); + } + + public override async Task Delete_Delete_with_same_entity_type(bool async) + { + await base.Delete_Delete_with_same_entity_type(async); + + AssertSql( + """ +@p0='1' +@p1='2' + +DELETE FROM "WithSomeDatabaseGenerated" +WHERE "Id" = @p0; +DELETE FROM "WithSomeDatabaseGenerated" +WHERE "Id" = @p1; +"""); + } + + #endregion Two operations with same entity type + + #region Two operations with different entity types + + public override async Task Add_Add_with_different_entity_types_and_generated_values(bool async) + { + await base.Add_Add_with_different_entity_types_and_generated_values(async); + + AssertSql( + """ +@p0='1000' +@p1='1001' + +INSERT INTO "WithSomeDatabaseGenerated" ("Data2") +VALUES (@p0) +RETURNING "Id", "Data1"; +INSERT INTO "WithSomeDatabaseGenerated2" ("Data2") +VALUES (@p1) +RETURNING "Id", "Data1"; +"""); + } + + public override async Task Add_Add_with_different_entity_types_and_no_generated_values(bool async) + { + await base.Add_Add_with_different_entity_types_and_no_generated_values(async); + + AssertSql( + """ +@p0='100' +@p1='1000' +@p2='1000' +@p3='101' +@p4='1001' +@p5='1001' + +INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") +VALUES (@p0, @p1, @p2); +INSERT INTO "WithNoDatabaseGenerated2" ("Id", "Data1", "Data2") +VALUES (@p3, @p4, @p5); +"""); + } + + public override async Task Add_Add_with_different_entity_types_and_all_generated_values(bool async) + { + await base.Add_Add_with_different_entity_types_and_all_generated_values(async); + + AssertSql( + """ +INSERT INTO "WithAllDatabaseGenerated" +DEFAULT VALUES +RETURNING "Id", "Data1", "Data2"; +INSERT INTO "WithAllDatabaseGenerated2" +DEFAULT VALUES +RETURNING "Id", "Data1", "Data2"; +"""); + } + + public override async Task Modify_Modify_with_different_entity_types_and_generated_values(bool async) + { + await base.Modify_Modify_with_different_entity_types_and_generated_values(async); + + AssertSql( + """ +@p1='1' +@p0='1000' +@p3='2' +@p2='1001' + +UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p0 +WHERE "Id" = @p1 +RETURNING "Data1"; +UPDATE "WithSomeDatabaseGenerated2" SET "Data2" = @p2 +WHERE "Id" = @p3 +RETURNING "Data1"; +"""); + } + + public override async Task Modify_Modify_with_different_entity_types_and_no_generated_values(bool async) + { + await base.Modify_Modify_with_different_entity_types_and_no_generated_values(async); + + AssertSql( + """ +@p2='1' +@p0='1000' +@p1='1000' +@p5='2' +@p3='1001' +@p4='1001' + +UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p0, "Data2" = @p1 +WHERE "Id" = @p2; +UPDATE "WithNoDatabaseGenerated2" SET "Data1" = @p3, "Data2" = @p4 +WHERE "Id" = @p5; +"""); + } + + public override async Task Delete_Delete_with_different_entity_types(bool async) + { + await base.Delete_Delete_with_different_entity_types(async); + + AssertSql( + """ +@p0='1' +@p1='2' + +DELETE FROM "WithSomeDatabaseGenerated" +WHERE "Id" = @p0; +DELETE FROM "WithSomeDatabaseGenerated2" +WHERE "Id" = @p1; +"""); + } + + #endregion Two operations with different entity types + + public class StoreValueGenerationGaussDBFixture : StoreValueGenerationFixtureBase + { + private string? _cleanDataSql; + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + foreach (var name in new[] + { + nameof(StoreValueGenerationContext.WithSomeDatabaseGenerated), + nameof(StoreValueGenerationContext.WithSomeDatabaseGenerated2), + nameof(StoreValueGenerationContext.WithAllDatabaseGenerated), + nameof(StoreValueGenerationContext.WithAllDatabaseGenerated2) + }) + { + ConfigureComputedColumn(modelBuilder.SharedTypeEntity(name).Property(w => w.Data1)); + } + + foreach (var name in new[] + { + nameof(StoreValueGenerationContext.WithAllDatabaseGenerated), + nameof(StoreValueGenerationContext.WithAllDatabaseGenerated2) + }) + { + ConfigureComputedColumn(modelBuilder.SharedTypeEntity(name).Property(w => w.Data2)); + } + + void ConfigureComputedColumn(PropertyBuilder builder) + { + if (TestEnvironment.PostgresVersion >= new Version(12, 0)) + { + // PG 12+ supports computed columns, but only stored (must be explicitly specified) + builder.Metadata.SetIsStored(true); + } + else + { + // Before PG 12, disable computed columns (but leave OnAddOrUpdate) + builder + .HasComputedColumnSql(null) + .HasDefaultValue(100) + .Metadata + .ValueGenerated = ValueGenerated.OnAddOrUpdate; + } + } + } + + public override void CleanData() + { + using var context = CreateContext(); + context.Database.ExecuteSqlRaw(_cleanDataSql ??= GetCleanDataSql()); + } + + private string GetCleanDataSql() + { + var context = CreateContext(); + var builder = new StringBuilder(); + + var helper = context.GetService(); + var tables = context.Model.GetEntityTypes() + .SelectMany(e => e.GetTableMappings().Select(m => helper.DelimitIdentifier(m.Table.Name, m.Table.Schema))); + + foreach (var table in tables) + { + builder.AppendLine($"TRUNCATE TABLE {table} RESTART IDENTITY;"); + } + + return builder.ToString(); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/Update/StoredProcedureUpdateGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/Update/StoredProcedureUpdateGaussDBTest.cs new file mode 100644 index 0000000000..b13f500b80 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/Update/StoredProcedureUpdateGaussDBTest.cs @@ -0,0 +1,565 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; + +namespace Microsoft.EntityFrameworkCore.Update; + +[MinimumPostgresVersion(14, 0)] +public class StoredProcedureUpdateGaussDBTest(NonSharedFixture fixture) : StoredProcedureUpdateTestBase(fixture) +{ + public override async Task Insert_with_output_parameter(bool async) + { + await base.Insert_with_output_parameter( + async, + """ +CREATE PROCEDURE "Entity_Insert"(name text, OUT id int) LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; +END $$ +"""); + + AssertSql( + """ +@p0='New' + +CALL "Entity_Insert"(@p0, NULL); +"""); + } + + public override async Task Insert_twice_with_output_parameter(bool async) + { + await base.Insert_twice_with_output_parameter( + async, + """ +CREATE PROCEDURE "Entity_Insert"(name text, OUT id int) LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; +END $$ +"""); + + AssertSql( + """ +@p0='New1' +@p1='New2' + +CALL "Entity_Insert"(@p0, NULL); +CALL "Entity_Insert"(@p1, NULL); +"""); + } + + public override async Task Insert_with_result_column(bool async) + { + var exception = + await Assert.ThrowsAsync(() => base.Insert_with_result_column(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Insert"), exception.Message); + } + + public override async Task Insert_with_two_result_columns(bool async) + { + var exception = + await Assert.ThrowsAsync(() => base.Insert_with_two_result_columns(async, createSprocSql: "")); + + Assert.Equal( + GaussDBStrings.StoredProcedureResultColumnsNotSupported( + nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Insert"), exception.Message); + } + + public override async Task Insert_with_output_parameter_and_result_column(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Insert_with_output_parameter_and_result_column(async, createSprocSql: "")); + + Assert.Equal( + GaussDBStrings.StoredProcedureResultColumnsNotSupported( + nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Insert"), exception.Message); + } + + public override async Task Update(bool async) + { + await base.Update( + async, + """ +CREATE PROCEDURE "Entity_Update"(id int, name text) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "Entity" SET "Name" = name WHERE "Id" = id; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1='Updated' + +CALL "Entity_Update"(@p0, @p1); +"""); + } + + public override async Task Update_partial(bool async) + { + await base.Update_partial( + async, + """ +CREATE PROCEDURE "EntityWithAdditionalProperty_Update"(id int, name text, additional_property int) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "EntityWithAdditionalProperty" SET "Name" = name, "AdditionalProperty" = additional_property WHERE "Id" = id; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1='Updated' +@p2='8' + +CALL "EntityWithAdditionalProperty_Update"(@p0, @p1, @p2); +"""); + } + + public override async Task Update_with_output_parameter_and_rows_affected_result_column(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Update_with_output_parameter_and_rows_affected_result_column(async, createSprocSql: "")); + + Assert.Equal( + GaussDBStrings.StoredProcedureResultColumnsNotSupported( + nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Update"), exception.Message); + } + + public override async Task Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(async, createSprocSql: "")); + + Assert.Equal( + GaussDBStrings.StoredProcedureResultColumnsNotSupported( + nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Update"), exception.Message); + } + + public override async Task Delete(bool async) + { + await base.Delete( + async, + """ +CREATE PROCEDURE "Entity_Delete"(id int) LANGUAGE plpgsql AS $$ +BEGIN + DELETE FROM "Entity" WHERE "Id" = id; +END $$ +"""); + + AssertSql( + """ +@p0='1' + +CALL "Entity_Delete"(@p0); +"""); + } + + public override async Task Delete_and_insert(bool async) + { + await base.Delete_and_insert( + async, + """ +CREATE PROCEDURE "Entity_Insert"(name text, OUT id int) LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; +END $$; + +CREATE PROCEDURE "Entity_Delete"(id int) LANGUAGE plpgsql AS $$ +BEGIN + DELETE FROM "Entity" WHERE "Id" = id; +END $$; +"""); + + AssertSql( + """ +@p0='1' +@p1='Entity2' + +CALL "Entity_Delete"(@p0); +CALL "Entity_Insert"(@p1, NULL); +"""); + } + + public override async Task Rows_affected_parameter(bool async) + { + await base.Rows_affected_parameter( + async, + """ +CREATE PROCEDURE "Entity_Update"(id int, name text, OUT rows_affected int) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "Entity" SET "Name" = name WHERE "Id" = id; + GET DIAGNOSTICS rows_affected = ROW_COUNT; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1='Updated' + +CALL "Entity_Update"(@p0, @p1, NULL); +"""); + } + + public override async Task Rows_affected_parameter_and_concurrency_failure(bool async) + { + await base.Rows_affected_parameter_and_concurrency_failure( + async, + """ +CREATE PROCEDURE "Entity_Update"(id int, name text, OUT rows_affected int) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "Entity" SET "Name" = name WHERE "Id" = id; + GET DIAGNOSTICS rows_affected = ROW_COUNT; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1='Updated' + +CALL "Entity_Update"(@p0, @p1, NULL); +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Rows_affected_parameter_with_another_output_parameter(bool async) + { + // PG doesn't supposed non-stored computed columns, so we need to duplicate the test code + var createSprocSql = + """ +CREATE PROCEDURE "EntityWithAdditionalProperty_Update"(id int, OUT rows_affected int, OUT additional_property int, name text) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "EntityWithAdditionalProperty" SET "Name" = name WHERE "Id" = id RETURNING "AdditionalProperty" INTO additional_property; + GET DIAGNOSTICS rows_affected = ROW_COUNT; +END $$ +"""; + + var contextFactory = await InitializeAsync( + modelBuilder => modelBuilder.Entity() + .UpdateUsingStoredProcedure( + nameof(EntityWithAdditionalProperty) + "_Update", + spb => spb + .HasOriginalValueParameter(w => w.Id) + .HasRowsAffectedParameter() + .HasParameter(w => w.AdditionalProperty, pb => pb.IsOutput()) + .HasParameter(w => w.Name)) + .Property(w => w.AdditionalProperty).HasComputedColumnSql("8", stored: true), + seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); + + await using var context = contextFactory.CreateContext(); + + var entity = new EntityWithAdditionalProperty { Name = "Initial" }; + context.Set().Add(entity); + await context.SaveChangesAsync(); + + ClearLog(); + + entity.Name = "Updated"; + entity.AdditionalProperty = 10; + + if (async) + { + await context.SaveChangesAsync(); + } + else + { + // ReSharper disable once MethodHasAsyncOverload + context.SaveChanges(); + } + + using (TestSqlLoggerFactory.SuspendRecordingEvents()) + { + var loadedEntity = await context.Set().SingleAsync(w => w.Id == entity.Id); + Assert.Equal("Updated", loadedEntity.Name); + Assert.Equal(8, loadedEntity.AdditionalProperty); + } + + AssertSql( + """ +@p0='1' +@p1='Updated' + +CALL "EntityWithAdditionalProperty_Update"(@p0, NULL, NULL, @p1); +"""); + } + + public override async Task Rows_affected_result_column(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Rows_affected_result_column(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); + } + + public override async Task Rows_affected_result_column_and_concurrency_failure(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Rows_affected_result_column_and_concurrency_failure(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); + } + + public override async Task Rows_affected_return_value(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Rows_affected_return_value(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureReturnValueNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); + } + + public override async Task Rows_affected_return_value_and_concurrency_failure(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Rows_affected_return_value(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureReturnValueNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); + } + + public override async Task Store_generated_concurrency_token_as_in_out_parameter(bool async) + { + await base.Store_generated_concurrency_token_as_in_out_parameter( + async, + """ +CREATE PROCEDURE "Entity_Update"(id int, INOUT concurrency_token xid, name text, OUT rows_affected int) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "Entity" SET "Name" = name WHERE "Id" = id AND xmin = concurrency_token RETURNING xmin INTO concurrency_token; + GET DIAGNOSTICS rows_affected = ROW_COUNT; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1=NULL (Direction = InputOutput) (DbType = Object) +@p2='Updated' + +CALL "Entity_Update"(@p0, @p1, @p2, NULL); +"""); + } + + public override async Task Store_generated_concurrency_token_as_two_parameters(bool async) + { + await base.Store_generated_concurrency_token_as_two_parameters( + async, + """ +CREATE PROCEDURE "Entity_Update"(id int, concurrency_token_in xid, name text, OUT concurrency_token_out xid, OUT rows_affected int) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "Entity" SET "Name" = name WHERE "Id" = id AND xmin = concurrency_token_in RETURNING xmin INTO concurrency_token_out; + GET DIAGNOSTICS rows_affected = ROW_COUNT; +END $$ +"""); + + // Can't assert SQL baseline as usual because the concurrency token changes + Assert.Equal( + """ +@p2='Updated' + +CALL "Entity_Update"(@p0, @p1, @p2, NULL, NULL); +""", + TestSqlLoggerFactory.Sql.Substring(TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), + ignoreLineEndingDifferences: true); + } + + public override async Task User_managed_concurrency_token(bool async) + { + await base.User_managed_concurrency_token( + async, + """ +CREATE PROCEDURE "EntityWithAdditionalProperty_Update"(id int, concurrency_token_original int, name text, concurrency_token_current int, OUT rows_affected int) LANGUAGE plpgsql AS $$ +BEGIN + UPDATE "EntityWithAdditionalProperty" SET "Name" = name, "AdditionalProperty" = concurrency_token_current WHERE "Id" = id AND "AdditionalProperty" = concurrency_token_original; + GET DIAGNOSTICS rows_affected = ROW_COUNT; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1='8' +@p2='Updated' +@p3='9' + +CALL "EntityWithAdditionalProperty_Update"(@p0, @p1, @p2, @p3, NULL); +"""); + } + + public override async Task Original_and_current_value_on_non_concurrency_token(bool async) + { + await base.Original_and_current_value_on_non_concurrency_token( + async, + """ +CREATE PROCEDURE "Entity_Update"(id int, name_current text, name_original text) LANGUAGE plpgsql AS $$ +BEGIN + IF name_current <> name_original THEN + UPDATE "Entity" SET "Name" = name_current WHERE "Id" = id; + END IF; +END $$ +"""); + + AssertSql( + """ +@p0='1' +@p1='Updated' +@p2='Initial' + +CALL "Entity_Update"(@p0, @p1, @p2); +"""); + } + + public override async Task Input_or_output_parameter_with_input(bool async) + { + await base.Input_or_output_parameter_with_input( + async, + """ +CREATE PROCEDURE "Entity_Insert"(OUT id int, INOUT name text) LANGUAGE plpgsql AS $$ +BEGIN + IF name IS NULL THEN + INSERT INTO "Entity" ("Name") VALUES ('Some default value') RETURNING "Id", "Name" INTO id, name; + ELSE + INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; + name = NULL; + END IF; +END $$ +"""); + + AssertSql( + """ +@p0='1' (Direction = InputOutput) (DbType = String) + +CALL "Entity_Insert"(NULL, @p0); +"""); + } + + public override async Task Input_or_output_parameter_with_output(bool async) + { + await base.Input_or_output_parameter_with_output( + async, + """ +CREATE PROCEDURE "Entity_Insert"(OUT id int, INOUT name text) LANGUAGE plpgsql AS $$ +BEGIN + IF name IS NULL THEN + INSERT INTO "Entity" ("Name") VALUES ('Some default value') RETURNING "Id", "Name" INTO id, name; + ELSE + INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; + name = NULL; + END IF; +END $$ +"""); + + AssertSql( + """ +@p0='1' (Direction = InputOutput) (DbType = String) + +CALL "Entity_Insert"(NULL, @p0); +"""); + } + + public override async Task Tph(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Rows_affected_result_column(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); + } + + public override async Task Tpt(bool async) + { + var exception = + await Assert.ThrowsAsync( + () => base.Rows_affected_result_column(async, createSprocSql: "")); + + Assert.Equal(GaussDBStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); + } + + public override async Task Tpt_mixed_sproc_and_non_sproc(bool async) + { + await base.Tpt_mixed_sproc_and_non_sproc( + async, + """ +CREATE PROCEDURE "Parent_Insert"(OUT id int, name text) LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO "Parent" ("Name") VALUES (name) RETURNING "Id" INTO id; +END $$ +"""); + + AssertSql( + """ +@p0='Child' + +CALL "Parent_Insert"(NULL, @p0); +""", + // + """ +@p1='1' +@p2='8' + +INSERT INTO "Child1" ("Id", "Child1Property") +VALUES (@p1, @p2); +"""); + } + + public override async Task Tpc(bool async) + { + await base.Tpc( + async, + """ +CREATE PROCEDURE "Child1_Insert"(OUT id int, name text, child1_property int) LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO "Child1" ("Name", "Child1Property") VALUES (name, child1_property) RETURNING "Id" INTO id; +END $$ +"""); + + AssertSql( + """ +@p0='Child' +@p1='8' + +CALL "Child1_Insert"(NULL, @p0, @p1); +"""); + } + + public override async Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async) + { + await base.Non_sproc_followed_by_sproc_commands_in_the_same_batch( + async, + """ +CREATE PROCEDURE "EntityWithAdditionalProperty_Insert"(name text, OUT id int, additional_property int) LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO "EntityWithAdditionalProperty" ("Name", "AdditionalProperty") VALUES (name, additional_property) RETURNING "Id" INTO id; +END $$ +"""); + + AssertSql( + """ +@p2='1' +@p0='2' +@p3='1' +@p1='Entity1_Modified' +@p4='Entity2' +@p5='0' + +UPDATE "EntityWithAdditionalProperty" SET "AdditionalProperty" = @p0, "Name" = @p1 +WHERE "Id" = @p2 AND "AdditionalProperty" = @p3; +CALL "EntityWithAdditionalProperty_Insert"(@p4, NULL, @p5); +"""); + } + + protected override void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName) + => entityTypeBuilder.Property(propertyName) + .HasColumnName("xmin") + .HasColumnType("xid") + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; +} diff --git a/test/EFCore.GaussDB.FunctionalTests/UpdatesGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/UpdatesGaussDBTest.cs new file mode 100644 index 0000000000..ae135bd4c0 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/UpdatesGaussDBTest.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; + +namespace Microsoft.EntityFrameworkCore; + +public class UpdatesGaussDBTest : UpdatesRelationalTestBase +{ + // ReSharper disable once UnusedParameter.Local + public UpdatesGaussDBTest(UpdatesGaussDBFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + public override void Identifiers_are_generated_correctly() + { + using var context = CreateContext(); + + var entityType = context.Model.FindEntityType( + typeof( + LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly + ))!; + Assert.Equal( + "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatI~", + entityType.GetTableName()); + Assert.Equal( + "PK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameTh~", + entityType.GetKeys().Single().GetName()); + Assert.Equal( + "FK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameTh~", + entityType.GetForeignKeys().Single().GetConstraintName()); + Assert.Equal( + "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameTh~", + entityType.GetIndexes().Single().GetDatabaseName()); + + var entityType2 = context.Model.FindEntityType( + typeof( + LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails + ))!; + + Assert.Equal( + "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThat~1", + entityType2.GetTableName()); + Assert.Equal( + "PK_LoginDetails", + entityType2.GetKeys().Single().GetName()); + Assert.Equal( + "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsU~", + entityType2.GetProperties().ElementAt(1).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); + Assert.Equal( + "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIs~1", + entityType2.GetProperties().ElementAt(2).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); + Assert.Equal( + "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameT~1", + entityType2.GetIndexes().Single().GetDatabaseName()); + } + + public class UpdatesGaussDBFixture : UpdatesRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("uuid-ossp"); + + 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().Property(r => r.Concurrency).HasColumnType("timestamp without time zone"); + } + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/ValueConvertersEndToEndGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/ValueConvertersEndToEndGaussDBTest.cs new file mode 100644 index 0000000000..6395b99ad6 --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/ValueConvertersEndToEndGaussDBTest.cs @@ -0,0 +1,219 @@ +namespace Microsoft.EntityFrameworkCore; + +public class ValueConvertersEndToEndGaussDBTest(ValueConvertersEndToEndGaussDBTest.ValueConvertersEndToEndGaussDBFixture fixture) + : ValueConvertersEndToEndTestBase(fixture) +{ + [ConditionalTheory(Skip = "DateTime and DateTimeOffset, https://github.com/dotnet/efcore/issues/26068")] + public override Task Can_insert_and_read_back_with_conversions(int[] valueOrder) + => base.Can_insert_and_read_back_with_conversions(valueOrder); + + [ConditionalTheory] + [InlineData(nameof(ConvertingEntity.BoolAsChar), "character(1)", false)] + [InlineData(nameof(ConvertingEntity.BoolAsNullableChar), "character(1)", false)] + [InlineData(nameof(ConvertingEntity.BoolAsString), "character varying(3)", false)] + [InlineData(nameof(ConvertingEntity.BoolAsInt), "integer", false)] + [InlineData(nameof(ConvertingEntity.BoolAsNullableString), "character varying(3)", false)] + [InlineData(nameof(ConvertingEntity.BoolAsNullableInt), "integer", false)] + [InlineData(nameof(ConvertingEntity.IntAsLong), "bigint", false)] + [InlineData(nameof(ConvertingEntity.IntAsNullableLong), "bigint", false)] + [InlineData(nameof(ConvertingEntity.BytesAsString), "text", false)] + [InlineData(nameof(ConvertingEntity.BytesAsNullableString), "text", false)] + [InlineData(nameof(ConvertingEntity.CharAsString), "character varying(1)", false)] + [InlineData(nameof(ConvertingEntity.CharAsNullableString), "character varying(1)", false)] + [InlineData(nameof(ConvertingEntity.DateTimeOffsetToBinary), "bigint", false)] + [InlineData(nameof(ConvertingEntity.DateTimeOffsetToNullableBinary), "bigint", false)] + [InlineData(nameof(ConvertingEntity.DateTimeOffsetToString), "character varying(48)", false)] + [InlineData(nameof(ConvertingEntity.DateTimeOffsetToNullableString), "character varying(48)", false)] + [InlineData(nameof(ConvertingEntity.DateTimeToBinary), "bigint", false)] + [InlineData(nameof(ConvertingEntity.DateTimeToNullableBinary), "bigint", false)] + [InlineData(nameof(ConvertingEntity.DateTimeToString), "character varying(48)", false)] + [InlineData(nameof(ConvertingEntity.DateTimeToNullableString), "character varying(48)", false)] + [InlineData(nameof(ConvertingEntity.EnumToString), "text", false)] + [InlineData(nameof(ConvertingEntity.EnumToNullableString), "text", false)] + [InlineData(nameof(ConvertingEntity.EnumToNumber), "bigint", false)] + [InlineData(nameof(ConvertingEntity.EnumToNullableNumber), "bigint", false)] + [InlineData(nameof(ConvertingEntity.GuidToString), "character varying(36)", false)] + [InlineData(nameof(ConvertingEntity.GuidToNullableString), "character varying(36)", false)] + [InlineData(nameof(ConvertingEntity.GuidToBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.GuidToNullableBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.IPAddressToString), "character varying(45)", false)] + [InlineData(nameof(ConvertingEntity.IPAddressToNullableString), "character varying(45)", false)] + [InlineData(nameof(ConvertingEntity.IPAddressToBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.IPAddressToNullableBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.PhysicalAddressToString), "character varying(20)", false)] + [InlineData(nameof(ConvertingEntity.PhysicalAddressToNullableString), "character varying(20)", false)] + [InlineData(nameof(ConvertingEntity.PhysicalAddressToBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.PhysicalAddressToNullableBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.NumberToString), "character varying(64)", false)] + [InlineData(nameof(ConvertingEntity.NumberToNullableString), "character varying(64)", false)] + [InlineData(nameof(ConvertingEntity.NumberToBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.NumberToNullableBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.StringToBool), "boolean", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableBool), "boolean", false)] + [InlineData(nameof(ConvertingEntity.StringToBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableBytes), "bytea", false)] + [InlineData(nameof(ConvertingEntity.StringToChar), "character(1)", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableChar), "character(1)", false)] + [InlineData(nameof(ConvertingEntity.StringToDateTime), "timestamp with time zone", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableDateTime), "timestamp with time zone", false)] + // [InlineData(nameof(ConvertingEntity.StringToDateTimeOffset), "timestamp with time zone", false)] + // [InlineData(nameof(ConvertingEntity.StringToNullableDateTimeOffset), "timestamp with time zone", false)] + [InlineData(nameof(ConvertingEntity.StringToEnum), "integer", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableEnum), "integer", false)] + [InlineData(nameof(ConvertingEntity.StringToGuid), "uuid", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableGuid), "uuid", false)] + [InlineData(nameof(ConvertingEntity.StringToNumber), "smallint", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableNumber), "smallint", false)] + [InlineData(nameof(ConvertingEntity.StringToTimeSpan), "interval", false)] + [InlineData(nameof(ConvertingEntity.StringToNullableTimeSpan), "interval", false)] + [InlineData(nameof(ConvertingEntity.TimeSpanToTicks), "bigint", false)] + [InlineData(nameof(ConvertingEntity.TimeSpanToNullableTicks), "bigint", false)] + [InlineData(nameof(ConvertingEntity.TimeSpanToString), "character varying(48)", false)] + [InlineData(nameof(ConvertingEntity.TimeSpanToNullableString), "character varying(48)", false)] + [InlineData(nameof(ConvertingEntity.UriToString), "text", false)] + [InlineData(nameof(ConvertingEntity.UriToNullableString), "text", false)] + [InlineData(nameof(ConvertingEntity.NullableCharAsString), "character varying(1)", true)] + [InlineData(nameof(ConvertingEntity.NullableCharAsNullableString), "character varying(1)", true)] + [InlineData(nameof(ConvertingEntity.NullableBoolAsChar), "character(1)", true)] + [InlineData(nameof(ConvertingEntity.NullableBoolAsNullableChar), "character(1)", true)] + [InlineData(nameof(ConvertingEntity.NullableBoolAsString), "character varying(3)", true)] + [InlineData(nameof(ConvertingEntity.NullableBoolAsNullableString), "character varying(3)", true)] + [InlineData(nameof(ConvertingEntity.NullableBoolAsInt), "integer", true)] + [InlineData(nameof(ConvertingEntity.NullableBoolAsNullableInt), "integer", true)] + [InlineData(nameof(ConvertingEntity.NullableIntAsLong), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableIntAsNullableLong), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableBytesAsString), "text", true)] + [InlineData(nameof(ConvertingEntity.NullableBytesAsNullableString), "text", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToBinary), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToNullableBinary), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToString), "character varying(48)", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToNullableString), "character varying(48)", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeToBinary), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeToNullableBinary), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeToString), "character varying(48)", true)] + [InlineData(nameof(ConvertingEntity.NullableDateTimeToNullableString), "character varying(48)", true)] + [InlineData(nameof(ConvertingEntity.NullableEnumToString), "text", true)] + [InlineData(nameof(ConvertingEntity.NullableEnumToNullableString), "text", true)] + [InlineData(nameof(ConvertingEntity.NullableEnumToNumber), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableEnumToNullableNumber), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableGuidToString), "character varying(36)", true)] + [InlineData(nameof(ConvertingEntity.NullableGuidToNullableString), "character varying(36)", true)] + [InlineData(nameof(ConvertingEntity.NullableGuidToBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableGuidToNullableBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableIPAddressToString), "character varying(45)", true)] + [InlineData(nameof(ConvertingEntity.NullableIPAddressToNullableString), "character varying(45)", true)] + [InlineData(nameof(ConvertingEntity.NullableIPAddressToBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableIPAddressToNullableBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToString), "character varying(20)", true)] + [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToNullableString), "character varying(20)", true)] + [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToNullableBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableNumberToString), "character varying(64)", true)] + [InlineData(nameof(ConvertingEntity.NullableNumberToNullableString), "character varying(64)", true)] + [InlineData(nameof(ConvertingEntity.NullableNumberToBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableNumberToNullableBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToBool), "boolean", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableBool), "boolean", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableBytes), "bytea", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToChar), "character(1)", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableChar), "character(1)", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToDateTime), "timestamp with time zone", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableDateTime), "timestamp with time zone", true)] + //[InlineData(nameof(ConvertingEntity.NullableStringToDateTimeOffset), "timestamp with time zone", true)] + //[InlineData(nameof(ConvertingEntity.NullableStringToNullableDateTimeOffset), "timestamp with time zone", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToEnum), "integer", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableEnum), "integer", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToGuid), "uuid", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableGuid), "uuid", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNumber), "smallint", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableNumber), "smallint", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToTimeSpan), "interval", true)] + [InlineData(nameof(ConvertingEntity.NullableStringToNullableTimeSpan), "interval", true)] + [InlineData(nameof(ConvertingEntity.NullableTimeSpanToTicks), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableTimeSpanToNullableTicks), "bigint", true)] + [InlineData(nameof(ConvertingEntity.NullableTimeSpanToString), "character varying(48)", true)] + [InlineData(nameof(ConvertingEntity.NullableTimeSpanToNullableString), "character varying(48)", true)] + [InlineData(nameof(ConvertingEntity.NullableUriToString), "text", true)] + [InlineData(nameof(ConvertingEntity.NullableUriToNullableString), "text", true)] + [InlineData(nameof(ConvertingEntity.NullStringToNonNullString), "text", false)] + [InlineData(nameof(ConvertingEntity.NonNullStringToNullString), "text", true)] + public virtual void Properties_with_conversions_map_to_appropriately_null_columns( + string propertyName, + string databaseType, + bool isNullable) + { + using var context = CreateContext(); + + var property = context.Model.FindEntityType(typeof(ConvertingEntity))!.FindProperty(propertyName); + + Assert.Equal(databaseType, property!.GetColumnType()); + Assert.Equal(isNullable, property!.IsNullable); + } + + [ConditionalFact] + public async Task Can_insert_and_read_back_with_value_converted_array() + { + await using var ctx = CreateContext(); + + var entity = new ValueConvertedArrayEntity { Values = [new(8), new(9)] }; + ctx.Add(entity); + await ctx.SaveChangesAsync(); + + var id = entity.Id; + ctx.ChangeTracker.Clear(); + + entity = await ctx.Set().SingleAsync(v => v.Id == id); + Assert.Equal(8, entity.Values[0].Value); + Assert.Equal(9, entity.Values[1].Value); + } + + public class ValueConvertersEndToEndGaussDBFixture : ValueConvertersEndToEndFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => + { + // We map DateTimeOffset to PG 'timestamp with time zone', which doesn't store the time zone - so lossless + // round-tripping is impossible. + b.Ignore(e => e.StringToDateTimeOffset); + b.Ignore(e => e.StringToNullableDateTimeOffset); + b.Ignore(e => e.NullableStringToDateTimeOffset); + b.Ignore(e => e.NullableStringToNullableDateTimeOffset); + }); + + // Add some GaussDB-specific value conversion scenarios + modelBuilder.Entity() + .PrimitiveCollection(x => x.Values) + .ElementType(e => e.HasConversion(typeof(IntWrapperConverter))); + } + + private class IntWrapperConverter() : ValueConverter(iw => iw.Value, i => new IntWrapper(i)); + } + + public class ValueConvertedArrayEntity + { + public int Id { get; set; } + public IntWrapper[] Values { get; set; } = null!; + } + + public class IntWrapper(int value) : IEquatable + { + public int Value { get; } = value; + + public bool Equals(IntWrapper? other) + => other is not null && Value == other.Value; + + public override bool Equals(object? obj) + => obj is IntWrapper other && Equals(other); + + public override int GetHashCode() + => Value.GetHashCode(); + } +} diff --git a/test/EFCore.GaussDB.FunctionalTests/WithConstructorsGaussDBTest.cs b/test/EFCore.GaussDB.FunctionalTests/WithConstructorsGaussDBTest.cs new file mode 100644 index 0000000000..18050d790f --- /dev/null +++ b/test/EFCore.GaussDB.FunctionalTests/WithConstructorsGaussDBTest.cs @@ -0,0 +1,23 @@ +namespace Microsoft.EntityFrameworkCore; + +public class WithConstructorsGaussDBTest(WithConstructorsGaussDBTest.WithConstructorsGaussDBFixture fixture) + : WithConstructorsTestBase(fixture) +{ + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class WithConstructorsGaussDBFixture : WithConstructorsFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => GaussDBTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().HasNoKey().ToSqlQuery(""" + SELECT * FROM "Blog" + """); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/config.json b/test/EFCore.GaussDB.FunctionalTests/config.json similarity index 88% rename from test/EFCore.PG.FunctionalTests/config.json rename to test/EFCore.GaussDB.FunctionalTests/config.json index 4b361bbec3..add290b03b 100644 --- a/test/EFCore.PG.FunctionalTests/config.json +++ b/test/EFCore.GaussDB.FunctionalTests/config.json @@ -1,6 +1,6 @@ { "Test": { - "Npgsql": { + "GaussDB": { "DefaultConnection": "Server=localhost;Username=npgsql_tests;Password=npgsql_tests;SSL Mode=disable" } } diff --git a/test/EFCore.GaussDB.Tests/Design/Internal/GaussDBAnnotationCodeGeneratorTest.cs b/test/EFCore.GaussDB.Tests/Design/Internal/GaussDBAnnotationCodeGeneratorTest.cs new file mode 100644 index 0000000000..c65d91c6ac --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Design/Internal/GaussDBAnnotationCodeGeneratorTest.cs @@ -0,0 +1,412 @@ +using Microsoft.EntityFrameworkCore.Storage.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Design.Internal; + +public class GaussDBAnnotationCodeGeneratorTest +{ + #region Identity / sequence / HiLo + + [Fact] + public void GenerateFluentApi_value_generation() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.Entity( + "Post", + x => + { + x.Property("IdentityByDefault").UseIdentityByDefaultColumn(); + x.Property("IdentityAlways").UseIdentityAlwaysColumn(); + x.Property("Serial").UseSerialColumn(); + x.Property("None").Metadata.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.None); + }); + + // Note: both serial and identity-by-default columns are considered by-convention - we don't want + // to assume that the GaussDB version of the scaffolded database necessarily determines the + // version of the database that the scaffolded model will target. This makes life difficult for + // models with mixed strategies but that's an edge case. + + var entity = (IEntityType)modelBuilder.Model.FindEntityType("Post"); + + var property = entity.GetProperties().Single(p => p.Name == "IdentityByDefault"); + var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + generator.RemoveAnnotationsHandledByConventions(property, annotations); + Assert.Empty(annotations); + var result = generator.GenerateFluentApiCalls(property, property.GetAnnotations().ToDictionary(a => a.Name, a => a)) + .Single(); + Assert.Equal(nameof(GaussDBPropertyBuilderExtensions.UseIdentityByDefaultColumn), result.Method); + Assert.Empty(result.Arguments); + + property = entity.GetProperties().Single(p => p.Name == "IdentityAlways"); + annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + generator.RemoveAnnotationsHandledByConventions(property, annotations); + Assert.Contains(annotations, kv => kv.Key == GaussDBAnnotationNames.ValueGenerationStrategy); + result = generator.GenerateFluentApiCalls(property, annotations).Single(); + Assert.Equal(nameof(GaussDBPropertyBuilderExtensions.UseIdentityAlwaysColumn), result.Method); + Assert.Empty(result.Arguments); + + property = entity.GetProperties().Single(p => p.Name == "Serial"); + annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + generator.RemoveAnnotationsHandledByConventions(property, annotations); + Assert.Empty(annotations); + result = generator.GenerateFluentApiCalls(property, property.GetAnnotations().ToDictionary(a => a.Name, a => a)) + .Single(); + Assert.Equal(nameof(GaussDBPropertyBuilderExtensions.UseSerialColumn), result.Method); + Assert.Empty(result.Arguments); + + property = entity.GetProperties().Single(p => p.Name == "None"); + annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + generator.RemoveAnnotationsHandledByConventions(property, annotations); + Assert.Contains(annotations, kv => kv.Key == GaussDBAnnotationNames.ValueGenerationStrategy); + result = generator.GenerateFluentApiCalls(property, property.GetAnnotations().ToDictionary(a => a.Name, a => a)) + .Single(); + Assert.Equal(nameof(PropertyBuilder.HasAnnotation), result.Method); + Assert.Collection( + result.Arguments, + a => Assert.Equal(GaussDBAnnotationNames.ValueGenerationStrategy, a), + a => Assert.Equal(GaussDBValueGenerationStrategy.None, a)); + } + + [Fact] + public void GenerateFluentApi_identity_sequence_options() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.Entity( + "Post", + x => + { + x.Property("Id") + .UseIdentityByDefaultColumn() + .HasIdentityOptions( + startValue: 5, + incrementBy: 2, + minValue: 3, + maxValue: 2000, + cyclic: true, + numbersToCache: 10); + }); + + var property = (IProperty)modelBuilder.Model.FindEntityType("Post").GetProperties() + .Single(p => p.Name == "Id"); + var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + generator.RemoveAnnotationsHandledByConventions(property, annotations); + Assert.Contains(annotations, kv => kv.Key == GaussDBAnnotationNames.IdentityOptions); + var result = generator.GenerateFluentApiCalls(property, annotations).Single(); + Assert.Equal(nameof(GaussDBPropertyBuilderExtensions.HasIdentityOptions), result.Method); + Assert.Equal(5L, result.Arguments[0]); + Assert.Equal(2L, result.Arguments[1]); + Assert.Equal(3L, result.Arguments[2]); + Assert.Equal(2000L, result.Arguments[3]); + Assert.Equal(true, result.Arguments[4]); + Assert.Equal(10L, result.Arguments[5]); + } + + [ConditionalFact] + public void GenerateFluentApi_IModel_works_with_IdentityByDefault() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.UseIdentityByDefaultColumns(); + + var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(((IModel)modelBuilder.Model), annotations).Single(); + + Assert.Equal("UseIdentityByDefaultColumns", result.Method); + Assert.Equal("GaussDBModelBuilderExtensions", result.DeclaringType); + + Assert.Empty(result.Arguments); + } + + [ConditionalFact] + public void GenerateFluentApi_IProperty_works_with_IdentityByDefault() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.Entity("Post", x => x.Property("Id").UseIdentityByDefaultColumn()); + var property = (IProperty)modelBuilder.Model.FindEntityType("Post").FindProperty("Id"); + + var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(property, annotations).Single(); + + Assert.Equal("UseIdentityByDefaultColumn", result.Method); + Assert.Equal("GaussDBPropertyBuilderExtensions", result.DeclaringType); + + Assert.Empty(result.Arguments); + } + + [ConditionalFact] + public void GenerateFluentApi_IModel_works_with_IdentityAlways() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.UseIdentityAlwaysColumns(); + + var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(((IModel)modelBuilder.Model), annotations).Single(); + + Assert.Equal("UseIdentityAlwaysColumns", result.Method); + Assert.Equal("GaussDBModelBuilderExtensions", result.DeclaringType); + + Assert.Empty(result.Arguments); + } + + [ConditionalFact] + public void GenerateFluentApi_IProperty_works_with_IdentityAlways() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.Entity("Post", x => x.Property("Id").UseIdentityAlwaysColumn()); + var property = (IProperty)modelBuilder.Model.FindEntityType("Post").FindProperty("Id"); + + var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(property, annotations).Single(); + + Assert.Equal("UseIdentityAlwaysColumn", result.Method); + Assert.Equal("GaussDBPropertyBuilderExtensions", result.DeclaringType); + + Assert.Empty(result.Arguments); + } + + [ConditionalFact] + public void GenerateFluentApi_IModel_works_with_HiLo() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.UseHiLo("HiLoIndexName", "HiLoIndexSchema"); + + var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls((IModel)modelBuilder.Model, annotations).Single(); + + Assert.Equal("UseHiLo", result.Method); + Assert.Equal("GaussDBModelBuilderExtensions", result.DeclaringType); + + Assert.Collection( + result.Arguments, + name => Assert.Equal("HiLoIndexName", name), + schema => Assert.Equal("HiLoIndexSchema", schema)); + } + + [ConditionalFact] + public void GenerateFluentApi_IProperty_works_with_HiLo() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.Entity("Post", x => x.Property("Id").UseHiLo("HiLoIndexName", "HiLoIndexSchema")); + var property = (IProperty)modelBuilder.Model.FindEntityType("Post").FindProperty("Id"); + + var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(property, annotations).Single(); + + Assert.Equal("UseHiLo", result.Method); + Assert.Equal("GaussDBPropertyBuilderExtensions", result.DeclaringType); + + Assert.Collection( + result.Arguments, + name => Assert.Equal("HiLoIndexName", name), + schema => Assert.Equal("HiLoIndexSchema", schema)); + } + + #endregion Identity / sequence / HiLo + + #region GaussDB extensions + + [ConditionalFact] + public void Extension() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.HasPostgresExtension("postgis"); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresExtension)); + + Assert.Collection(result.Arguments, name => Assert.Equal("postgis", name)); + } + + [ConditionalFact] + public void Extension_with_schema() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.HasPostgresExtension("some_schema", "postgis"); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresExtension)); + + Assert.Collection( + result.Arguments, + schema => Assert.Equal("some_schema", schema), + name => Assert.Equal("postgis", name)); + } + + [ConditionalFact] + public void Extension_with_null_schema() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + modelBuilder.HasPostgresExtension(null, "postgis"); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresExtension)); + + Assert.Collection(result.Arguments, name => Assert.Equal("postgis", name)); + } + + #endregion GaussDB extensions + + #region Enum + + [ConditionalFact] + public void Enum() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + + var enumLabels = new[] { "someValue1", "someValue2" }; + modelBuilder.HasPostgresEnum("some_enum", enumLabels); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresEnum)); + + Assert.Collection( + result.Arguments, + name => Assert.Equal("some_enum", name), + labels => Assert.Equal(enumLabels, labels)); + } + + [ConditionalFact] + public void Enum_with_schema() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + + var enumLabels = new[] { "someValue1", "someValue2" }; + modelBuilder.HasPostgresEnum("some_schema", "some_enum", enumLabels); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresEnum)); + + Assert.Collection( + result.Arguments, + schema => Assert.Equal("some_schema", schema), + name => Assert.Equal("some_enum", name), + labels => Assert.Equal(enumLabels, labels)); + } + + [ConditionalFact] + public void Enum_with_null_schema() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + + var enumLabels = new[] { "someValue1", "someValue2" }; + modelBuilder.HasPostgresEnum(schema: null, "some_enum", enumLabels); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresEnum)); + + Assert.Collection( + result.Arguments, + name => Assert.Equal("some_enum", name), + labels => Assert.Equal(enumLabels, labels)); + } + + #endregion Enum + + #region Range + + [ConditionalFact] + public void Range() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + + modelBuilder.HasPostgresRange("some_range", "some_subtype"); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresRange)); + + Assert.Collection( + result.Arguments, + name => Assert.Equal("some_range", name), + subtype => Assert.Equal("some_subtype", subtype)); + } + + [ConditionalFact] + public void Range_with_schema() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + + modelBuilder.HasPostgresRange("some_schema", "some_range", "some_subtype"); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresRange)); + + Assert.Collection( + result.Arguments, + schema => Assert.Equal("some_schema", schema), + name => Assert.Equal("some_range", name), + subtype => Assert.Equal("some_subtype", subtype), + canonicalFunction => Assert.Null(canonicalFunction), + subtypeOpClass => Assert.Null(subtypeOpClass), + collation => Assert.Null(collation), + subtypeDiff => Assert.Null(subtypeDiff)); + } + + [ConditionalFact] + public void Range_with_null_schema() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(GaussDBConventionSetBuilder.Build()); + + modelBuilder.HasPostgresRange(schema: null, "some_range", "some_subtype"); + + var model = (IModel)modelBuilder.Model; + var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(model, annotations) + .Single(c => c.Method == nameof(GaussDBModelBuilderExtensions.HasPostgresRange)); + + Assert.Collection( + result.Arguments, + name => Assert.Equal("some_range", name), + subtype => Assert.Equal("some_subtype", subtype)); + } + + #endregion Range + + private GaussDBAnnotationCodeGenerator CreateGenerator() + => new( + new AnnotationCodeGeneratorDependencies( + new GaussDBTypeMappingSource( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + [] + ), + new RelationalTypeMappingSourceDependencies([]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions()))); +} diff --git a/test/EFCore.GaussDB.Tests/EFCore.GaussDB.Tests.csproj b/test/EFCore.GaussDB.Tests/EFCore.GaussDB.Tests.csproj new file mode 100644 index 0000000000..f65ff19d96 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/EFCore.GaussDB.Tests.csproj @@ -0,0 +1,28 @@ + + + + HuaweiCloud.EntityFrameworkCore.GaussDB.Tests + HuaweiCloud.EntityFrameworkCore.GaussDB + disable + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/EFCore.GaussDB.Tests/GaussDBDatabaseFacadeTest.cs b/test/EFCore.GaussDB.Tests/GaussDBDatabaseFacadeTest.cs new file mode 100644 index 0000000000..f6204a3d59 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBDatabaseFacadeTest.cs @@ -0,0 +1,165 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +public class GaussDBDatabaseFacadeTest +{ + [Fact] + public void IsGaussDB_when_using_OnConfiguring() + { + using var context = new GaussDBOnConfiguringContext(); + + Assert.True(context.Database.IsGaussDB()); + } + + [Fact] + public void IsGaussDB_in_OnModelCreating_when_using_OnConfiguring() + { + using var context = new GaussDBOnModelContext(); + var _ = context.Model; // Trigger context initialization + + Assert.True(context.IsGaussDBSet); + } + + [Fact] + public void IsGaussDB_in_constructor_when_using_OnConfiguring() + { + using var context = new GaussDBConstructorContext(); + var _ = context.Model; // Trigger context initialization + + Assert.True(context.IsGaussDBSet); + } + + [Fact] + public void Cannot_use_IsGaussDB_in_OnConfiguring() + { + using var context = new GaussDBUseInOnConfiguringContext(); + + Assert.Equal( + CoreStrings.RecursiveOnConfiguring, + Assert.Throws( + () => + { + var _ = context.Model; // Trigger context initialization + }).Message); + } + + [Fact] + public void IsGaussDB_when_using_constructor() + { + using var context = new ProviderContext( + new DbContextOptionsBuilder().UseGaussDB("Database=Maltesers").Options); + + Assert.True(context.Database.IsGaussDB()); + } + + [Fact] + public void IsGaussDB_in_OnModelCreating_when_using_constructor() + { + using var context = new ProviderOnModelContext( + new DbContextOptionsBuilder().UseGaussDB("Database=Maltesers").Options); + var _ = context.Model; // Trigger context initialization + + Assert.True(context.IsGaussDBSet); + } + + [Fact] + public void IsGaussDB_in_constructor_when_using_constructor() + { + using var context = new ProviderConstructorContext( + new DbContextOptionsBuilder().UseGaussDB("Database=Maltesers").Options); + var _ = context.Model; // Trigger context initialization + + Assert.True(context.IsGaussDBSet); + } + + [Fact] + public void Cannot_use_IsGaussDB_in_OnConfiguring_with_constructor() + { + using var context = new ProviderUseInOnConfiguringContext( + new DbContextOptionsBuilder().UseGaussDB("Database=Maltesers").Options); + + Assert.Equal( + CoreStrings.RecursiveOnConfiguring, + Assert.Throws( + () => + { + var _ = context.Model; // Trigger context initialization + }).Message); + } + + /* + [Fact] + public void Not_IsGaussDB_when_using_different_provider() + { + using var context = new ProviderContext( + new DbContextOptionsBuilder().UseInMemoryDatabase("Maltesers").Options); + + Assert.False(context.Database.IsGaussDB()); + }*/ + + private class ProviderContext : DbContext + { + protected ProviderContext() + { + } + + public ProviderContext(DbContextOptions options) + : base(options) + { + } + + public bool? IsGaussDBSet { get; protected set; } + } + + private class GaussDBOnConfiguringContext : ProviderContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB("Database=Maltesers"); + } + + private class GaussDBOnModelContext : GaussDBOnConfiguringContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => IsGaussDBSet = Database.IsGaussDB(); + } + + private class GaussDBConstructorContext : GaussDBOnConfiguringContext + { + // ReSharper disable once VirtualMemberCallInConstructor + public GaussDBConstructorContext() + { + IsGaussDBSet = Database.IsGaussDB(); + } + } + + private class GaussDBUseInOnConfiguringContext : GaussDBOnConfiguringContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + IsGaussDBSet = Database.IsGaussDB(); + } + } + + private class ProviderOnModelContext(DbContextOptions options) : ProviderContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => IsGaussDBSet = Database.IsGaussDB(); + } + + private class ProviderConstructorContext : ProviderContext + { + // ReSharper disable once VirtualMemberCallInConstructor + public ProviderConstructorContext(DbContextOptions options) + : base(options) + { + IsGaussDBSet = Database.IsGaussDB(); + } + } + + private class ProviderUseInOnConfiguringContext(DbContextOptions options) : ProviderContext(options) + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => IsGaussDBSet = Database.IsGaussDB(); + } +} diff --git a/test/EFCore.GaussDB.Tests/GaussDBDbContextOptionsExtensionsTest.cs b/test/EFCore.GaussDB.Tests/GaussDBDbContextOptionsExtensionsTest.cs new file mode 100644 index 0000000000..33e32188c7 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBDbContextOptionsExtensionsTest.cs @@ -0,0 +1,134 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +public class GaussDBDbContextOptionsExtensionsTest +{ + [ConditionalFact] + public void Can_add_extension_with_max_batch_size() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB("Database=Crunchie", b => b.MaxBatchSize(123)); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Equal(123, extension.MaxBatchSize); + } + + [ConditionalFact] + public void Can_add_extension_with_command_timeout() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB("Database=Crunchie", b => b.CommandTimeout(30)); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Equal(30, extension.CommandTimeout); + } + + [ConditionalFact] + public void Can_add_extension_with_connection_string() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB("Database=Crunchie"); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Equal("Database=Crunchie", extension.ConnectionString); + Assert.Null(extension.Connection); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_add_extension_with_connection_string_using_generic_options(bool nullConnectionString) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB(nullConnectionString ? null : "Database=Whisper"); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Equal(nullConnectionString ? null : "Database=Whisper", extension.ConnectionString); + Assert.Null(extension.Connection); + } + + [ConditionalFact] + public void Can_add_extension_with_connection() + { + var optionsBuilder = new DbContextOptionsBuilder(); + var connection = new GaussDBConnection(); + + optionsBuilder.UseGaussDB(connection); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Same(connection, extension.Connection); + Assert.Null(extension.ConnectionString); + } + + [ConditionalFact] + public void Can_add_extension_with_connection_using_generic_options() + { + var optionsBuilder = new DbContextOptionsBuilder(); + var connection = new GaussDBConnection(); + + optionsBuilder.UseGaussDB(connection); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Same(connection, extension.Connection); + Assert.Null(extension.ConnectionString); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Service_collection_extension_method_can_configure_npgsql_options(bool nullConnectionString) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddGaussDB( + nullConnectionString ? null : "Database=Crunchie", + npgsqlOption => + { + npgsqlOption.MaxBatchSize(123); + npgsqlOption.CommandTimeout(30); + }, + dbContextOption => + { + dbContextOption.EnableDetailedErrors(); + }); + + var services = serviceCollection.BuildServiceProvider(); + + using (var serviceScope = services + .GetRequiredService() + .CreateScope()) + { + var coreOptions = serviceScope.ServiceProvider.GetRequiredService>() + .GetExtension(); + Assert.True(coreOptions.DetailedErrorsEnabled); + + var npgsqlOptions = serviceScope.ServiceProvider.GetRequiredService>() + .GetExtension(); + Assert.Equal(123, npgsqlOptions.MaxBatchSize); + Assert.Equal(30, npgsqlOptions.CommandTimeout); + Assert.Equal(nullConnectionString ? null : "Database=Crunchie", npgsqlOptions.ConnectionString); + } + } + + [ConditionalFact] + public void Varying_data_source_connection_strings_do_not_cause_multiple_service_providers() + { + for (var i = 0; i < 21; i++) + { + var optionsBuilder = new DbContextOptionsBuilder(); + var dataSource = GaussDBDataSource.Create($"Host=localhost;Database={i};Include Error Detail=true;Username=admin;Password=admin"); + optionsBuilder.UseGaussDB(dataSource); + + using var context = new ApplicationDbContext(optionsBuilder.Options); + _ = context.Model; + } + } + + private class ApplicationDbContext(DbContextOptions options) : DbContext(options); +} diff --git a/test/EFCore.GaussDB.Tests/GaussDBMigrationBuilderTest.cs b/test/EFCore.GaussDB.Tests/GaussDBMigrationBuilderTest.cs new file mode 100644 index 0000000000..ffcefa4319 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBMigrationBuilderTest.cs @@ -0,0 +1,18 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +public class GaussDBMigrationBuilderTest +{ + [Fact] + public void IsGaussDB_when_using_GaussDB() + { + var migrationBuilder = new MigrationBuilder("HuaweiCloud.EntityFrameworkCore.GaussDB"); + Assert.True(migrationBuilder.IsGaussDB()); + } + + [Fact] + public void Not_IsGaussDB_when_using_different_provider() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.InMemory"); + Assert.False(migrationBuilder.IsGaussDB()); + } +} diff --git a/test/EFCore.GaussDB.Tests/GaussDBNetTopologySuiteApiConsistencyTest.cs b/test/EFCore.GaussDB.Tests/GaussDBNetTopologySuiteApiConsistencyTest.cs new file mode 100644 index 0000000000..a25df5a86e --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBNetTopologySuiteApiConsistencyTest.cs @@ -0,0 +1,18 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +public class GaussDBNetTopologySuiteApiConsistencyTest( + GaussDBNetTopologySuiteApiConsistencyTest.GaussDBNetTopologySuiteApiConsistencyFixture fixture) + : ApiConsistencyTestBase(fixture) +{ + protected override void AddServices(ServiceCollection serviceCollection) + => serviceCollection.AddEntityFrameworkGaussDBNetTopologySuite(); + + protected override Assembly TargetAssembly + => typeof(GaussDBNetTopologySuiteServiceCollectionExtensions).Assembly; + + public class GaussDBNetTopologySuiteApiConsistencyFixture : ApiConsistencyFixtureBase + { + public override HashSet FluentApiTypes { get; } = + [typeof(GaussDBNetTopologySuiteDbContextOptionsBuilderExtensions), typeof(GaussDBNetTopologySuiteServiceCollectionExtensions)]; + } +} diff --git a/test/EFCore.GaussDB.Tests/GaussDBNodaTimeApiConsistencyTest.cs b/test/EFCore.GaussDB.Tests/GaussDBNodaTimeApiConsistencyTest.cs new file mode 100644 index 0000000000..23f3763bc8 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBNodaTimeApiConsistencyTest.cs @@ -0,0 +1,17 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +public class GaussDBNodaTimeApiConsistencyTest(GaussDBNodaTimeApiConsistencyTest.GaussDBNodaTimeApiConsistencyFixture fixture) + : ApiConsistencyTestBase(fixture) +{ + protected override void AddServices(ServiceCollection serviceCollection) + => serviceCollection.AddEntityFrameworkGaussDBNodaTime(); + + protected override Assembly TargetAssembly + => typeof(GaussDBNodaTimeServiceCollectionExtensions).Assembly; + + public class GaussDBNodaTimeApiConsistencyFixture : ApiConsistencyFixtureBase + { + public override HashSet FluentApiTypes { get; } = + [typeof(GaussDBNodaTimeDbContextOptionsBuilderExtensions), typeof(GaussDBNodaTimeServiceCollectionExtensions)]; + } +} diff --git a/test/EFCore.GaussDB.Tests/GaussDBRelationalConnectionTest.cs b/test/EFCore.GaussDB.Tests/GaussDBRelationalConnectionTest.cs new file mode 100644 index 0000000000..dbcc0d2058 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBRelationalConnectionTest.cs @@ -0,0 +1,455 @@ +using System.Data.Common; +using System.Transactions; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Diagnostics.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities.FakeProvider; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +#nullable enable + +public class GaussDBRelationalConnectionTest +{ + [Fact] + public void Creates_GaussDBConnection() + { + using var connection = CreateConnection(); + + Assert.IsType(connection.DbConnection); + } + + [Fact] + public void Uses_DbDataSource_from_DbContextOptions() + { + using var dataSource = GaussDBDataSource.Create("Host=FakeHost"); + + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddGaussDBDataSource("Host=FakeHost") + // ReSharper disable once AccessToDisposedClosure + .AddDbContext(o => o.UseGaussDB(dataSource)); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + using var scope1 = serviceProvider.CreateScope(); + var context1 = scope1.ServiceProvider.GetRequiredService(); + var relationalConnection1 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Same(dataSource, relationalConnection1.DbDataSource); + + var connection1 = context1.GetService().Database.GetDbConnection(); + Assert.Equal("Host=FakeHost", connection1.ConnectionString); + + using var scope2 = serviceProvider.CreateScope(); + var context2 = scope2.ServiceProvider.GetRequiredService(); + var relationalConnection2 = (GaussDBRelationalConnection)context2.GetService(); + Assert.Same(dataSource, relationalConnection2.DbDataSource); + + var connection2 = context2.GetService().Database.GetDbConnection(); + Assert.Equal("Host=FakeHost", connection2.ConnectionString); + } + + [Fact] + public void Uses_DbDataSource_from_application_service_provider() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddGaussDBDataSource("Host=FakeHost") + .AddDbContext(o => o.UseGaussDB()); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var dataSource = serviceProvider.GetRequiredService(); + + using var scope1 = serviceProvider.CreateScope(); + var context1 = scope1.ServiceProvider.GetRequiredService(); + var relationalConnection1 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Same(dataSource, relationalConnection1.DbDataSource); + + var connection1 = context1.GetService().Database.GetDbConnection(); + Assert.Equal("Host=FakeHost", connection1.ConnectionString); + + using var scope2 = serviceProvider.CreateScope(); + var context2 = scope2.ServiceProvider.GetRequiredService(); + var relationalConnection2 = (GaussDBRelationalConnection)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 + .AddGaussDBDataSource("Host=FakeHost1") + .AddDbContext(o => o.UseGaussDB("Host=FakeHost2")); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + using var scope1 = serviceProvider.CreateScope(); + var context1 = scope1.ServiceProvider.GetRequiredService(); + var relationalConnection1 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite()); + var connection2 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost2", no => no.UseNetTopologySuite()); + var connection2 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", no => no.UseNodaTime()); + var connection2 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood")); + var connection2 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost2", no => no.MapEnum("mood")); + var connection2 = (GaussDBRelationalConnection)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 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1", _ => { /* no enums */}); + var connection2 = (GaussDBRelationalConnection)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 = GaussDBDataSource.Create("Host=FakeHost"); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB(dataSource, no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "foo")); + + var context1 = new FakeDbContext(optionsBuilder.Options); + var exception = Assert.Throws(() => context1.GetService()); + Assert.Equal(GaussDBStrings.DataSourceAndConfigNotSupported, exception.Message); + } + + [Fact] + public void Multiple_connection_strings_without_data_source_features() + { + var context1 = new ConfigurableContext("Host=FakeHost1"); + var connection1 = (GaussDBRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.Null(connection1.DbDataSource); + + var context2 = new ConfigurableContext("Host=FakeHost1"); + var connection2 = (GaussDBRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Null(connection2.DbDataSource); + + var context3 = new ConfigurableContext("Host=FakeHost2"); + var connection3 = (GaussDBRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.Null(connection3.DbDataSource); + } + + [Fact] + public void Can_create_master_connection_with_connection_string() + { + using var connection = CreateConnection(); + using var master = connection.CreateAdminConnection(); + + Assert.Equal( + @"Host=localhost;Database=postgres;Username=some_user;Password=some_password;Pooling=False;Multiplexing=False", + master.ConnectionString); + } + + [Fact] + public void Can_create_master_connection_with_connection_string_and_alternate_admin_db() + { + var options = new DbContextOptionsBuilder() + .UseGaussDB( + @"Host=localhost;Database=GaussDBConnectionTest;Username=some_user;Password=some_password", + b => b.UseAdminDatabase("template0")) + .Options; + + using var connection = CreateConnection(options); + using var master = connection.CreateAdminConnection(); + + Assert.Equal( + @"Host=localhost;Database=template0;Username=some_user;Password=some_password;Pooling=False;Multiplexing=False", + master.ConnectionString); + } + + [Theory] + [InlineData("false")] + [InlineData("False")] + [InlineData("FALSE")] + public void CurrentAmbientTransaction_returns_null_with_enlist_set_to_false(string falseValue) + { + var options = new DbContextOptionsBuilder() + .UseGaussDB( + @"Host=localhost;Database=GaussDBConnectionTest;Username=some_user;Password=some_password;Enlist=" + falseValue) + .Options; + + Transaction.Current = new CommittableTransaction(); + + using var connection = CreateConnection(options); + Assert.Null(connection.CurrentAmbientTransaction); + + Transaction.Current = null; + } + + [Theory] + [InlineData(";Enlist=true")] + [InlineData("")] // Enlist is true by default + public void CurrentAmbientTransaction_returns_transaction_with_enlist_enabled(string enlist) + { + var options = new DbContextOptionsBuilder() + .UseGaussDB( + @"Host=localhost;Database=GaussDBConnectionTest;Username=some_user;Password=some_password" + enlist) + .Options; + + var transaction = new CommittableTransaction(); + Transaction.Current = transaction; + + using var connection = CreateConnection(options); + Assert.Equal(transaction, connection.CurrentAmbientTransaction); + + Transaction.Current = null; + } + + [ConditionalFact] + public async Task CloneWith_with_connection_and_connection_string() + { + var services = GaussDBTestHelpers.Instance.CreateContextServices( + new DbContextOptionsBuilder() + .UseGaussDB("Host=localhost;Database=DummyDatabase") + .Options); + + var relationalConnection = (GaussDBRelationalConnection)services.GetRequiredService(); + + 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 GaussDBRelationalConnection CreateConnection(DbContextOptions? options = null, DbDataSource? dataSource = null) + { + options ??= new DbContextOptionsBuilder() + .UseGaussDB(@"Host=localhost;Database=GaussDBConnectionTest;Username=some_user;Password=some_password") + .Options; + + foreach (var extension in options.Extensions) + { + extension.Validate(options); + } + + var dbContextOptions = CreateOptions(); + + return new GaussDBRelationalConnection( + new RelationalConnectionDependencies( + options, + new DiagnosticsLogger( + new LoggerFactory(), + new LoggingOptions(), + new DiagnosticListener("FakeDiagnosticListener"), + new GaussDBLoggingDefinitions(), + new NullDbContextLogger()), + new RelationalConnectionDiagnosticsLogger( + new LoggerFactory(), + new LoggingOptions(), + new DiagnosticListener("FakeDiagnosticListener"), + new GaussDBLoggingDefinitions(), + new NullDbContextLogger(), + dbContextOptions), + new NamedConnectionStringResolver(options), + new RelationalTransactionFactory( + new RelationalTransactionFactoryDependencies( + new RelationalSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()))), + new CurrentDbContext(new FakeDbContext()), + new RelationalCommandBuilderFactory( + new RelationalCommandBuilderDependencies( + new GaussDBTypeMappingSource( + TestServiceFactory.Instance.Create(), + TestServiceFactory.Instance.Create(), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions()), + new ExceptionDetector(), + new LoggingOptions())), + new ExceptionDetector()), + new GaussDBDataSourceManager([]), + dbContextOptions); + } + + private const string ConnectionString = "Fake Connection String"; + + private static IDbContextOptions CreateOptions(RelationalOptionsExtension? optionsExtension = null) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder) + .AddOrUpdateExtension( + optionsExtension + ?? new FakeRelationalOptionsExtension().WithConnectionString(ConnectionString)); + + return optionsBuilder.Options; + } + + private class FakeDbContext : DbContext + { + public FakeDbContext() + { + } + + public FakeDbContext(DbContextOptions options) + : base(options) + { + } + } + + private class ConfigurableContext(string connectionString, Action? npgsqlOptionsAction = null) : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseGaussDB(connectionString, npgsqlOptionsAction); + } + + private enum Mood + { + // ReSharper disable once UnusedMember.Local + Happy, + // ReSharper disable once UnusedMember.Local + Sad + } +} diff --git a/test/EFCore.GaussDB.Tests/GaussDBValueGeneratorSelectorTest.cs b/test/EFCore.GaussDB.Tests/GaussDBValueGeneratorSelectorTest.cs new file mode 100644 index 0000000000..58123bcd8a --- /dev/null +++ b/test/EFCore.GaussDB.Tests/GaussDBValueGeneratorSelectorTest.cs @@ -0,0 +1,214 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; +using HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration; +using HuaweiCloud.EntityFrameworkCore.GaussDB.ValueGeneration.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB; + +public class GaussDBValueGeneratorSelectorTest +{ + [Fact] + public void Returns_built_in_generators_for_types_setup_for_value_generation() + { + AssertGenerator("Id"); + AssertGenerator("Custom"); + AssertGenerator("Long"); + AssertGenerator("Short"); + AssertGenerator("Byte"); + AssertGenerator("NullableInt"); + AssertGenerator("NullableLong"); + AssertGenerator("NullableShort"); + AssertGenerator("NullableByte"); + AssertGenerator("Decimal"); + AssertGenerator("String"); + AssertGenerator("Guid"); + AssertGenerator("Binary"); + } + + private void AssertGenerator(string propertyName, bool setSequences = false) + { + var builder = GaussDBTestHelpers.Instance.CreateConventionBuilder(); + builder.Entity( + b => + { + b.Property(e => e.Custom).HasValueGenerator(); + b.Property(propertyName).ValueGeneratedOnAdd(); + b.HasKey(propertyName); + }); + + if (setSequences) + { + builder.UseHiLo(); + Assert.NotNull(builder.Model.FindSequence(GaussDBModelExtensions.DefaultHiLoSequenceName)); + } + + var model = builder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(AnEntity)); + + var selector = GaussDBTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); + + var property = entityType.FindProperty(propertyName)!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + + Assert.IsType(generator); + } + + [ConditionalFact] + public void Returns_temp_guid_generator_when_default_sql_set() + { + var builder = GaussDBTestHelpers.Instance.CreateConventionBuilder(); + builder.Entity( + b => + { + b.Property(e => e.Guid).HasDefaultValueSql("newid()"); + b.HasKey(e => e.Guid); + }); + var model = builder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(AnEntity)); + + var selector = GaussDBTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); + + var property = entityType.FindProperty("Guid")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + Assert.IsType(generator); + } + + [ConditionalFact] + public void Returns_temp_string_generator_when_default_sql_set() + { + var builder = GaussDBTestHelpers.Instance.CreateConventionBuilder(); + builder.Entity( + b => + { + b.Property(e => e.String).ValueGeneratedOnAdd().HasDefaultValueSql("Foo"); + b.HasKey(e => e.String); + }); + var model = builder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(AnEntity)); + + var selector = GaussDBTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); + + var property = entityType.FindProperty("String")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + + Assert.IsType(generator); + Assert.True(generator.GeneratesTemporaryValues); + } + + [ConditionalFact] + public void Returns_temp_binary_generator_when_default_sql_set() + { + var builder = GaussDBTestHelpers.Instance.CreateConventionBuilder(); + builder.Entity( + b => + { + b.HasKey(e => e.Binary); + b.Property(e => e.Binary).HasDefaultValueSql("Foo").ValueGeneratedOnAdd(); + }); + var model = builder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(AnEntity)); + + var selector = GaussDBTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); + + var property = entityType.FindProperty("Binary")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + + Assert.IsType(generator); + Assert.True(generator.GeneratesTemporaryValues); + } + + [Fact] + public void Returns_sequence_value_generators_when_configured_for_model() + { + AssertGenerator>("Id", setSequences: true); + AssertGenerator("Custom", setSequences: true); + AssertGenerator>("Long", setSequences: true); + AssertGenerator>("Short", setSequences: true); + AssertGenerator>("NullableInt", setSequences: true); + AssertGenerator>("NullableLong", setSequences: true); + AssertGenerator>("NullableShort", setSequences: true); + AssertGenerator("String", setSequences: true); + AssertGenerator("Guid", setSequences: true); + AssertGenerator("Binary", setSequences: true); + } + +// [ConditionalFact] +// public void Throws_for_unsupported_combinations() +// { +// var builder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); +// builder.Entity( +// b => +// { +// b.Property(e => e.Random).ValueGeneratedOnAdd(); +// b.HasKey(e => e.Random); +// }); +// var model = builder.FinalizeModel(); +// var entityType = model.FindEntityType(typeof(AnEntity)); +// +// var selector = InMemoryTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); +// +// Assert.Equal( +// CoreStrings.NoValueGenerator("Random", "AnEntity", "Something"), +// Assert.Throws(() => selector.Select(entityType.FindProperty("Random"), entityType)).Message); +// } + + [ConditionalFact] + public void Returns_generator_configured_on_model_when_property_is_identity() + { + var builder = GaussDBTestHelpers.Instance.CreateConventionBuilder(); + + builder.Entity(); + + builder + .UseHiLo() + .HasSequence(GaussDBModelExtensions.DefaultHiLoSequenceName); + + var model = builder.UseHiLo().FinalizeModel(); + var entityType = model.FindEntityType(typeof(AnEntity)); + + var selector = GaussDBTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); + + var property = entityType.FindProperty("Id")!; + Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); + + Assert.IsType>(generator); + } + + private class AnEntity + { + // ReSharper disable UnusedMember.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + public int Id { get; set; } + public int Custom { get; set; } + public long Long { get; set; } + public short Short { get; set; } + public byte Byte { get; set; } + public char Char { get; set; } + public int? NullableInt { get; set; } + public long? NullableLong { get; set; } + public short? NullableShort { get; set; } + public byte? NullableByte { get; set; } + public char? NullableChar { get; set; } + public string String { get; set; } + public Guid Guid { get; set; } + public byte[] Binary { get; set; } + public float Float { get; set; } + public decimal Decimal { get; set; } + public int AlwaysIdentity { get; set; } + public int AlwaysSequence { get; set; } + + [NotMapped] + public Random Random { get; set; } + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class CustomValueGenerator : ValueGenerator + { + public override int Next(EntityEntry entry) + => throw new NotImplementedException(); + + public override bool GeneratesTemporaryValues + => false; + } +} diff --git a/test/EFCore.GaussDB.Tests/Metadata/Conventions/GaussDBPostgresModelFinalizingConventionTest.cs b/test/EFCore.GaussDB.Tests/Metadata/Conventions/GaussDBPostgresModelFinalizingConventionTest.cs new file mode 100644 index 0000000000..62619a6c04 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Metadata/Conventions/GaussDBPostgresModelFinalizingConventionTest.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +public class GaussDBPostgresModelFinalizingConventionTest +{ + [Fact] + public void RowVersion_properties_get_mapped_to_xmin() + { + var modelBuilder = GaussDBTestHelpers.Instance.CreateConventionBuilder(); + modelBuilder.Entity().Property(b => b.RowVersion).IsRowVersion(); + var model = modelBuilder.FinalizeModel(); + + var entityType = model.FindEntityType(typeof(Blog))!; + var property = entityType.FindProperty(nameof(Blog.RowVersion))!; + + Assert.Equal("xmin", property.GetColumnName()); + Assert.Equal("xid", property.GetColumnType()); + } + + private class Blog + { + public int Id { get; set; } + + [Timestamp] + public uint RowVersion { get; set; } + } +} diff --git a/test/EFCore.GaussDB.Tests/Metadata/Conventions/GaussDBValueGenerationStrategyConventionTest.cs b/test/EFCore.GaussDB.Tests/Metadata/Conventions/GaussDBValueGenerationStrategyConventionTest.cs new file mode 100644 index 0000000000..4488b72a86 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Metadata/Conventions/GaussDBValueGenerationStrategyConventionTest.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Conventions; + +public class GaussDBValueGenerationStrategyConventionTest +{ + [Fact] + public void Annotations_are_added_when_conventional_model_builder_is_used() + { + var model = GaussDBTestHelpers.Instance.CreateConventionBuilder().Model; + + var annotations = model.GetAnnotations().OrderBy(a => a.Name).ToList(); + Assert.Equal(3, annotations.Count); + + Assert.Equal(GaussDBAnnotationNames.ValueGenerationStrategy, annotations.First().Name); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, annotations.First().Value); + } + + [Fact] + public void Annotations_are_added_when_conventional_model_builder_is_used_with_sequences() + { + var model = GaussDBTestHelpers.Instance.CreateConventionBuilder() + .UseHiLo() + .Model; + + model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); + + var annotations = model.GetAnnotations().OrderBy(a => a.Name).ToList(); + Assert.Equal(4, annotations.Count); + + Assert.Equal(GaussDBAnnotationNames.HiLoSequenceName, annotations[0].Name); + Assert.Equal(GaussDBModelExtensions.DefaultHiLoSequenceName, annotations[0].Value); + + Assert.Equal(GaussDBAnnotationNames.ValueGenerationStrategy, annotations[1].Name); + Assert.Equal(GaussDBValueGenerationStrategy.SequenceHiLo, annotations[1].Value); + + Assert.Equal(RelationalAnnotationNames.MaxIdentifierLength, annotations[2].Name); + + Assert.Equal( + RelationalAnnotationNames.Sequences, + annotations[3].Name); + Assert.NotNull(annotations[3].Value); + } +} diff --git a/test/EFCore.GaussDB.Tests/Metadata/GaussDBBuilderExtensionsTest.cs b/test/EFCore.GaussDB.Tests/Metadata/GaussDBBuilderExtensionsTest.cs new file mode 100644 index 0000000000..2c2d3bb9a9 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Metadata/GaussDBBuilderExtensionsTest.cs @@ -0,0 +1,88 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +public class GaussDBBuilderExtensionsTest +{ + [Fact] + public void CockroachDbInterleaveInParent() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity() + .ToTable("customers", "my_schema") + .UseCockroachDbInterleaveInParent(typeof(Customer), ["col_a", "col_b"]); + + var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); + var interleaveInParent = entityType.GetCockroachDbInterleaveInParent(); + + Assert.Equal("my_schema", interleaveInParent.ParentTableSchema); + Assert.Equal("customers", interleaveInParent.ParentTableName); + var interleavePrefix = interleaveInParent.InterleavePrefix; + Assert.Equal(2, interleavePrefix.Count); + Assert.Equal("col_a", interleavePrefix[0]); + Assert.Equal("col_b", interleavePrefix[1]); + } + + [Fact] + public void Can_set_identity_sequence_options_on_property() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity() + .Property(e => e.Id) + .UseIdentityByDefaultColumn() + .HasIdentityOptions( + startValue: 5, + incrementBy: 2, + minValue: 3, + maxValue: 2000, + cyclic: true, + numbersToCache: 10); + + var model = modelBuilder.Model; + var property = model.FindEntityType(typeof(Customer)).FindProperty("Id"); + + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, property.GetValueGenerationStrategy()); + Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); + Assert.Equal(5, property.GetIdentityStartValue()); + Assert.Equal(3, property.GetIdentityMinValue()); + Assert.Equal(2000, property.GetIdentityMaxValue()); + Assert.Equal(2, property.GetIdentityIncrementBy()); + Assert.True(property.GetIdentityIsCyclic()); + Assert.Equal(10, property.GetIdentityNumbersToCache()); + + Assert.Null(model.FindSequence(GaussDBModelExtensions.DefaultHiLoSequenceName)); + Assert.Null(model.FindSequence(GaussDBModelExtensions.DefaultHiLoSequenceName)); + } + + protected virtual ModelBuilder CreateConventionModelBuilder() + => GaussDBTestHelpers.Instance.CreateConventionBuilder(); + + private class Customer + { + public int Id { get; set; } + public string Name { get; set; } + + public IEnumerable Orders { get; set; } + } + + private class Order + { + public int OrderId { get; set; } + + public int CustomerId { get; set; } + public Customer Customer { get; set; } + + public OrderDetails Details { get; set; } + } + + private class OrderDetails + { + public int Id { get; set; } + + public int OrderId { get; set; } + public Order Order { get; set; } + } +} diff --git a/test/EFCore.GaussDB.Tests/Metadata/GaussDBMetadataBuilderExtensionsTest.cs b/test/EFCore.GaussDB.Tests/Metadata/GaussDBMetadataBuilderExtensionsTest.cs new file mode 100644 index 0000000000..2f8dd3bab3 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Metadata/GaussDBMetadataBuilderExtensionsTest.cs @@ -0,0 +1,123 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +public class GaussDBInternalMetadataBuilderExtensionsTest +{ + private IConventionModelBuilder CreateBuilder() + => new InternalModelBuilder(new Model()); + + [ConditionalFact] + public void Can_access_model() + { + var builder = CreateBuilder(); + + Assert.NotNull( + builder + .HasValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo)); + Assert.Equal(GaussDBValueGenerationStrategy.SequenceHiLo, builder.Metadata.GetValueGenerationStrategy()); + + Assert.NotNull( + builder + .HasValueGenerationStrategy(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, fromDataAnnotation: true)); + Assert.Equal( + GaussDBValueGenerationStrategy.IdentityByDefaultColumn, builder.Metadata.GetValueGenerationStrategy()); + + Assert.Null( + builder + .HasValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo)); + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, builder.Metadata.GetValueGenerationStrategy()); + + Assert.Equal( + 1, builder.Metadata.GetAnnotations().Count( + a => a.Name.StartsWith(GaussDBAnnotationNames.Prefix, StringComparison.Ordinal))); + } + + [ConditionalFact] + public void Can_access_entity_type() + { + var typeBuilder = CreateBuilder().Entity(typeof(Splot)); + + Assert.NotNull(typeBuilder.IsUnlogged()); + Assert.True(typeBuilder.Metadata.GetIsUnlogged()); + + Assert.NotNull(typeBuilder.IsUnlogged(false, fromDataAnnotation: true)); + Assert.False(typeBuilder.Metadata.GetIsUnlogged()); + + Assert.Null(typeBuilder.IsUnlogged()); + Assert.False(typeBuilder.Metadata.GetIsUnlogged()); + + Assert.Equal( + 1, typeBuilder.Metadata.GetAnnotations().Count( + a => a.Name.StartsWith(GaussDBAnnotationNames.Prefix, StringComparison.Ordinal))); + } + + [ConditionalFact] + public void Can_access_property() + { + var propertyBuilder = CreateBuilder() + .Entity(typeof(Splot)) + .Property(typeof(int), "Id"); + + Assert.NotNull(propertyBuilder.HasHiLoSequence("Splew", null)); + Assert.Equal("Splew", propertyBuilder.Metadata.GetHiLoSequenceName()); + + Assert.NotNull(propertyBuilder.HasHiLoSequence("Splow", null, fromDataAnnotation: true)); + Assert.Equal("Splow", propertyBuilder.Metadata.GetHiLoSequenceName()); + + Assert.Null(propertyBuilder.HasHiLoSequence("Splod", null)); + Assert.Equal("Splow", propertyBuilder.Metadata.GetHiLoSequenceName()); + + Assert.Equal( + 1, propertyBuilder.Metadata.GetAnnotations().Count( + a => a.Name.StartsWith(GaussDBAnnotationNames.Prefix, StringComparison.Ordinal))); + } + + [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([idProperty]); + + Assert.NotNull(indexBuilder.HasMethod("gin")); + Assert.Equal("gin", indexBuilder.Metadata.GetMethod()); + + Assert.NotNull(indexBuilder.HasMethod("gist", fromDataAnnotation: true)); + Assert.Equal("gist", indexBuilder.Metadata.GetMethod()); + + Assert.Null(indexBuilder.HasMethod("gin")); + Assert.Equal("gist", indexBuilder.Metadata.GetMethod()); + + Assert.Equal( + 1, indexBuilder.Metadata.GetAnnotations().Count( + a => a.Name.StartsWith(GaussDBAnnotationNames.Prefix, StringComparison.Ordinal))); + } + + [ConditionalFact] + 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([idProperty]).Metadata; + var relationshipBuilder = entityTypeBuilder.HasRelationship(entityTypeBuilder.Metadata, key); + + Assert.NotNull(relationshipBuilder.HasConstraintName("Splew")); + Assert.Equal("Splew", relationshipBuilder.Metadata.GetConstraintName()); + + Assert.NotNull(relationshipBuilder.HasConstraintName("Splow", fromDataAnnotation: true)); + Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); + + Assert.Null(relationshipBuilder.HasConstraintName("Splod")); + Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); + + Assert.Equal( + 1, relationshipBuilder.Metadata.GetAnnotations().Count( + a => a.Name.StartsWith(RelationalAnnotationNames.Prefix, StringComparison.Ordinal))); + } + + private class Splot; +} diff --git a/test/EFCore.GaussDB.Tests/Metadata/GaussDBMetadataExtensionsTest.cs b/test/EFCore.GaussDB.Tests/Metadata/GaussDBMetadataExtensionsTest.cs new file mode 100644 index 0000000000..9b62a7fb5a --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Metadata/GaussDBMetadataExtensionsTest.cs @@ -0,0 +1,536 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Metadata; + +public class GaussDBMetadataExtensionsTest +{ + [ConditionalFact] + public void Can_get_and_set_column_name() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Name) + .Metadata; + + Assert.Equal("Name", property.GetColumnName()); + Assert.Null(((IConventionProperty)property).GetColumnNameConfigurationSource()); + + ((IConventionProperty)property).SetColumnName("Eman", fromDataAnnotation: true); + + Assert.Equal("Eman", property.GetColumnName()); + Assert.Equal(ConfigurationSource.DataAnnotation, ((IConventionProperty)property).GetColumnNameConfigurationSource()); + + property.SetColumnName("MyNameIs"); + + Assert.Equal("Name", property.Name); + Assert.Equal("MyNameIs", property.GetColumnName()); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionProperty)property).GetColumnNameConfigurationSource()); + + property.SetColumnName(null); + + Assert.Equal("Name", property.GetColumnName()); + Assert.Null(((IConventionProperty)property).GetColumnNameConfigurationSource()); + } + + [ConditionalFact] + public void Can_get_and_set_column_key_name() + { + var modelBuilder = GetModelBuilder(); + + var key = modelBuilder + .Entity() + .HasKey(e => e.Id) + .Metadata; + + Assert.Equal("PK_Customer", key.GetName()); + + key.SetName("PrimaryKey"); + + Assert.Equal("PrimaryKey", key.GetName()); + + key.SetName("PrimarySchool"); + + Assert.Equal("PrimarySchool", key.GetName()); + + key.SetName(null); + + Assert.Equal("PK_Customer", key.GetName()); + } + + [ConditionalFact] + public void Can_get_and_set_index_method() + { + var modelBuilder = GetModelBuilder(); + + var index = modelBuilder + .Entity() + .HasIndex(e => e.Id) + .Metadata; + + Assert.Null(index.GetMethod()); + + index.SetMethod("gin"); + + Assert.Equal("gin", index.GetMethod()); + + index.SetMethod(null); + + Assert.Null(index.GetMethod()); + } + + [ConditionalFact] + public void Can_get_and_set_sequence() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + Assert.Null(model.FindSequence("Foo")); + Assert.Null(model.FindSequence("Foo")); + Assert.Null(((IModel)model).FindSequence("Foo")); + + var sequence = model.AddSequence("Foo"); + + Assert.Equal("Foo", model.FindSequence("Foo").Name); + Assert.Equal("Foo", ((IModel)model).FindSequence("Foo").Name); + Assert.Equal("Foo", model.FindSequence("Foo").Name); + Assert.Equal("Foo", ((IModel)model).FindSequence("Foo").Name); + + Assert.Equal("Foo", sequence.Name); + Assert.Null(sequence.Schema); + Assert.Equal(1, sequence.IncrementBy); + Assert.Equal(1, sequence.StartValue); + Assert.Null(sequence.MinValue); + Assert.Null(sequence.MaxValue); + Assert.Same(typeof(long), sequence.Type); + + Assert.NotNull(model.FindSequence("Foo")); + + var sequence2 = model.FindSequence("Foo"); + + sequence.StartValue = 1729; + sequence.IncrementBy = 11; + sequence.MinValue = 2001; + sequence.MaxValue = 2010; + sequence.Type = typeof(int); + + Assert.Equal("Foo", sequence.Name); + Assert.Null(sequence.Schema); + Assert.Equal(11, sequence.IncrementBy); + Assert.Equal(1729, sequence.StartValue); + Assert.Equal(2001, sequence.MinValue); + Assert.Equal(2010, sequence.MaxValue); + Assert.Same(typeof(int), sequence.Type); + + Assert.Equal(sequence2.Name, sequence.Name); + Assert.Equal(sequence2.Schema, sequence.Schema); + Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); + Assert.Equal(sequence2.StartValue, sequence.StartValue); + Assert.Equal(sequence2.MinValue, sequence.MinValue); + Assert.Equal(sequence2.MaxValue, sequence.MaxValue); + Assert.Same(sequence2.Type, sequence.Type); + } + + [ConditionalFact] + public void Can_get_and_set_sequence_with_schema_name() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + Assert.Null(model.FindSequence("Foo", "Smoo")); + Assert.Null(model.FindSequence("Foo", "Smoo")); + Assert.Null(((IModel)model).FindSequence("Foo", "Smoo")); + + var sequence = model.AddSequence("Foo", "Smoo"); + + Assert.Equal("Foo", model.FindSequence("Foo", "Smoo").Name); + Assert.Equal("Foo", ((IModel)model).FindSequence("Foo", "Smoo").Name); + Assert.Equal("Foo", model.FindSequence("Foo", "Smoo").Name); + Assert.Equal("Foo", ((IModel)model).FindSequence("Foo", "Smoo").Name); + + Assert.Equal("Foo", sequence.Name); + Assert.Equal("Smoo", sequence.Schema); + Assert.Equal(1, sequence.IncrementBy); + Assert.Equal(1, sequence.StartValue); + Assert.Null(sequence.MinValue); + Assert.Null(sequence.MaxValue); + Assert.Same(typeof(long), sequence.Type); + + Assert.NotNull(model.FindSequence("Foo", "Smoo")); + + var sequence2 = model.FindSequence("Foo", "Smoo"); + + sequence.StartValue = 1729; + sequence.IncrementBy = 11; + sequence.MinValue = 2001; + sequence.MaxValue = 2010; + sequence.Type = typeof(int); + + Assert.Equal("Foo", sequence.Name); + Assert.Equal("Smoo", sequence.Schema); + Assert.Equal(11, sequence.IncrementBy); + Assert.Equal(1729, sequence.StartValue); + Assert.Equal(2001, sequence.MinValue); + Assert.Equal(2010, sequence.MaxValue); + Assert.Same(typeof(int), sequence.Type); + + Assert.Equal(sequence2.Name, sequence.Name); + Assert.Equal(sequence2.Schema, sequence.Schema); + Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); + Assert.Equal(sequence2.StartValue, sequence.StartValue); + Assert.Equal(sequence2.MinValue, sequence.MinValue); + Assert.Equal(sequence2.MaxValue, sequence.MaxValue); + Assert.Same(sequence2.Type, sequence.Type); + } + + [ConditionalFact] + public void Can_get_multiple_sequences() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + model.AddSequence("Fibonacci"); + model.AddSequence("Golomb"); + + var sequences = model.GetSequences(); + + Assert.Equal(2, sequences.Count()); + Assert.Contains(sequences, s => s.Name == "Fibonacci"); + Assert.Contains(sequences, s => s.Name == "Golomb"); + } + + [ConditionalFact] + public void Can_get_multiple_sequences_when_overridden() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + model.AddSequence("Fibonacci").StartValue = 1; + model.FindSequence("Fibonacci").StartValue = 3; + model.AddSequence("Golomb"); + + var sequences = model.GetSequences(); + + Assert.Equal(2, sequences.Count()); + Assert.Contains(sequences, s => s.Name == "Golomb"); + + var sequence = sequences.FirstOrDefault(s => s.Name == "Fibonacci"); + Assert.NotNull(sequence); + Assert.Equal(3, sequence.StartValue); + } + + [ConditionalFact] + public void Can_get_and_set_value_generation_on_model() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + // TODO for PG9.6 testing: make this conditional + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, model.GetValueGenerationStrategy()); + + model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal(GaussDBValueGenerationStrategy.SequenceHiLo, model.GetValueGenerationStrategy()); + + model.SetValueGenerationStrategy(null); + + Assert.Null(model.GetValueGenerationStrategy()); + } + + [ConditionalFact] + public void Can_get_and_set_default_sequence_name_on_model() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + model.SetHiLoSequenceName("Tasty.Snook"); + + Assert.Equal("Tasty.Snook", model.GetHiLoSequenceName()); + + model.SetHiLoSequenceName(null); + + Assert.Equal(GaussDBModelExtensions.DefaultHiLoSequenceName, model.GetHiLoSequenceName()); + } + + [ConditionalFact] + public void Can_get_and_set_default_sequence_schema_on_model() + { + var modelBuilder = GetModelBuilder(); + var model = modelBuilder.Model; + + Assert.Null(model.GetHiLoSequenceSchema()); + + model.SetHiLoSequenceSchema("Tasty.Snook"); + + Assert.Equal("Tasty.Snook", model.GetHiLoSequenceSchema()); + + model.SetHiLoSequenceSchema(null); + + Assert.Null(model.GetHiLoSequenceSchema()); + } + + [ConditionalFact] + public void Can_get_and_set_value_generation_on_property() + { + var modelBuilder = GetModelBuilder(); + modelBuilder.Model.SetValueGenerationStrategy(null); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .Metadata; + + Assert.Equal(GaussDBValueGenerationStrategy.None, property.GetValueGenerationStrategy()); + Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); + + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal(GaussDBValueGenerationStrategy.SequenceHiLo, property.GetValueGenerationStrategy()); + Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); + + property.SetValueGenerationStrategy(null); + + Assert.Equal(GaussDBValueGenerationStrategy.None, property.GetValueGenerationStrategy()); + Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); + } + + [ConditionalFact] + public void Can_get_and_set_value_generation_on_nullable_property() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.NullableInt).ValueGeneratedOnAdd() + .Metadata; + + // TODO for PG9.6 testing: make this conditional + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, property.GetValueGenerationStrategy()); + + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal(GaussDBValueGenerationStrategy.SequenceHiLo, property.GetValueGenerationStrategy()); + + property.SetValueGenerationStrategy(null); + + // TODO for PG9.6 testing: make this conditional + Assert.Equal(GaussDBValueGenerationStrategy.IdentityByDefaultColumn, property.GetValueGenerationStrategy()); + } + + [ConditionalFact] + public void Can_get_and_set_sequence_name_on_property() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .Metadata; + + Assert.Null(property.GetHiLoSequenceName()); + Assert.Null(((IProperty)property).GetHiLoSequenceName()); + + property.SetHiLoSequenceName("Snook"); + + Assert.Equal("Snook", property.GetHiLoSequenceName()); + + property.SetHiLoSequenceName(null); + + Assert.Null(property.GetHiLoSequenceName()); + } + + [ConditionalFact] + public void Can_get_and_set_sequence_schema_on_property() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .Metadata; + + Assert.Null(property.GetHiLoSequenceSchema()); + + property.SetHiLoSequenceSchema("Tasty"); + + Assert.Equal("Tasty", property.GetHiLoSequenceSchema()); + + property.SetHiLoSequenceSchema(null); + + Assert.Null(property.GetHiLoSequenceSchema()); + } + + [ConditionalFact] + public void TryGetSequence_returns_sequence_property_is_marked_for_sequence_generation() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw"); + property.SetHiLoSequenceName("DaneelOlivaw"); + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + } + + [ConditionalFact] + public void TryGetSequence_returns_sequence_property_is_marked_for_default_generation_and_model_is_marked_for_sequence_generation() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw"); + modelBuilder.Model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + property.SetHiLoSequenceName("DaneelOlivaw"); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + } + + [ConditionalFact] + public void TryGetSequence_returns_sequence_property_is_marked_for_sequence_generation_and_model_has_name() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw"); + modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + } + + [ConditionalFact] + public void + TryGetSequence_returns_sequence_property_is_marked_for_default_generation_and_model_is_marked_for_sequence_generation_and_model_has_name() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw"); + modelBuilder.Model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + } + + [ConditionalFact] + public void TryGetSequence_with_schema_returns_sequence_property_is_marked_for_sequence_generation() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); + property.SetHiLoSequenceName("DaneelOlivaw"); + property.SetHiLoSequenceSchema("R"); + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + Assert.Equal("R", property.FindHiLoSequence().Schema); + } + + [ConditionalFact] + public void TryGetSequence_with_schema_returns_sequence_model_is_marked_for_sequence_generation() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); + modelBuilder.Model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + property.SetHiLoSequenceName("DaneelOlivaw"); + property.SetHiLoSequenceSchema("R"); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + Assert.Equal("R", property.FindHiLoSequence().Schema); + } + + [ConditionalFact] + public void TryGetSequence_with_schema_returns_sequence_property_is_marked_for_sequence_generation_and_model_has_name() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); + modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); + modelBuilder.Model.SetHiLoSequenceSchema("R"); + property.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + Assert.Equal("R", property.FindHiLoSequence().Schema); + } + + [ConditionalFact] + public void TryGetSequence_with_schema_returns_sequence_model_is_marked_for_sequence_generation_and_model_has_name() + { + var modelBuilder = GetModelBuilder(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd() + .Metadata; + + modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); + modelBuilder.Model.SetValueGenerationStrategy(GaussDBValueGenerationStrategy.SequenceHiLo); + modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); + modelBuilder.Model.SetHiLoSequenceSchema("R"); + + Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); + Assert.Equal("R", property.FindHiLoSequence().Schema); + } + + private static ModelBuilder GetModelBuilder() + => GaussDBTestHelpers.Instance.CreateConventionBuilder(); + + // ReSharper disable once ClassNeverInstantiated.Local + private class Customer + { + public int Id { get; set; } + public int? NullableInt { get; set; } + public string Name { get; set; } + public byte Byte { get; set; } + public byte? NullableByte { get; set; } + public byte[] ByteArray { get; set; } + } + + private class Order + { + public int OrderId { get; set; } + public int CustomerId { get; set; } + } +} diff --git a/test/EFCore.GaussDB.Tests/Migrations/GaussDBHistoryRepositoryTest.cs b/test/EFCore.GaussDB.Tests/Migrations/GaussDBHistoryRepositoryTest.cs new file mode 100644 index 0000000000..ed23d18ad5 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Migrations/GaussDBHistoryRepositoryTest.cs @@ -0,0 +1,183 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Migrations; + +public class GaussDBHistoryRepositoryTest +{ + [ConditionalFact] + public void GetCreateScript_works() + { + var sql = CreateHistoryRepository().GetCreateScript(); + + Assert.Equal( + """ +CREATE TABLE "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetCreateScript_works_with_schema() + { + var sql = CreateHistoryRepository("my").GetCreateScript(); + + Assert.Equal( + """ +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'my') THEN + CREATE SCHEMA my; + END IF; +END $EF$; +CREATE TABLE my."__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetCreateIfNotExistsScript_works() + { + var sql = CreateHistoryRepository().GetCreateIfNotExistsScript(); + + Assert.Equal( + """ +CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetCreateIfNotExistsScript_works_with_schema() + { + var sql = CreateHistoryRepository("my").GetCreateIfNotExistsScript(); + + Assert.Equal( + """ +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'my') THEN + CREATE SCHEMA my; + END IF; +END $EF$; +CREATE TABLE IF NOT EXISTS my."__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetDeleteScript_works() + { + var sql = CreateHistoryRepository().GetDeleteScript("Migration1"); + + Assert.Equal( + """ +DELETE FROM "__EFMigrationsHistory" +WHERE "MigrationId" = 'Migration1'; + +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetInsertScript_works() + { + var sql = CreateHistoryRepository().GetInsertScript( + new HistoryRow("Migration1", "7.0.0")); + + Assert.Equal( + """ +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('Migration1', '7.0.0'); + +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetBeginIfNotExistsScript_works() + { + var sql = CreateHistoryRepository().GetBeginIfNotExistsScript("Migration1"); + + Assert.Equal( + """ + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = 'Migration1') THEN +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetBeginIfExistsScript_works() + { + var sql = CreateHistoryRepository().GetBeginIfExistsScript("Migration1"); + + Assert.Equal( + """ +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = 'Migration1') THEN +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetEndIfScript_works() + { + var sql = CreateHistoryRepository().GetEndIfScript(); + + Assert.Equal( + """ + END IF; +END $EF$; +""", sql, ignoreLineEndingDifferences: true); + } + + private static IHistoryRepository CreateHistoryRepository(string schema = null) + => new TestDbContext( + new DbContextOptionsBuilder() + .UseInternalServiceProvider(GaussDBTestHelpers.Instance.CreateServiceProvider()) + .UseGaussDB( + new GaussDBConnection("Host=localhost;Database=DummyDatabase"), + b => b.MigrationsHistoryTable(HistoryRepository.DefaultTableName, schema)) + .Options) + .GetService(); + + private class TestDbContext(DbContextOptions options) : DbContext(options) + { + public DbSet Blogs { get; set; } + + [DbFunction("TableFunction")] + public IQueryable TableFunction() + => FromExpression(() => TableFunction()); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + } + } + + private class Blog + { + public int Id { get; set; } + } + + private class TableFunction + { + public int Id { get; set; } + public int BlogId { get; set; } + public Blog Blog { get; set; } + } +} diff --git a/test/EFCore.PG.Tests/Properties/AssemblyInfo.cs b/test/EFCore.GaussDB.Tests/Properties/AssemblyInfo.cs similarity index 100% rename from test/EFCore.PG.Tests/Properties/AssemblyInfo.cs rename to test/EFCore.GaussDB.Tests/Properties/AssemblyInfo.cs diff --git a/test/EFCore.GaussDB.Tests/Scaffolding/GaussDBCodeGeneratorTest.cs b/test/EFCore.GaussDB.Tests/Scaffolding/GaussDBCodeGeneratorTest.cs new file mode 100644 index 0000000000..cb339c6f55 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Scaffolding/GaussDBCodeGeneratorTest.cs @@ -0,0 +1,102 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.NetTopologySuite.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.NodaTime.Scaffolding.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Scaffolding; + +public class GaussDBCodeGeneratorTest +{ + [Fact] + public virtual void Use_provider_method_is_generated_correctly() + { + var codeGenerator = new GaussDBCodeGenerator( + new ProviderCodeGeneratorDependencies( + [])); + + var result = codeGenerator.GenerateUseProvider("Server=test;Username=test;Password=test;Database=test", providerOptions: null); + + Assert.Equal("UseGaussDB", result.Method); + Assert.Collection( + result.Arguments, + a => Assert.Equal("Server=test;Username=test;Password=test;Database=test", a)); + Assert.Null(result.ChainedCall); + } + + [Fact] + public virtual void Use_provider_method_is_generated_correctly_with_options() + { + var codeGenerator = new GaussDBCodeGenerator( + new ProviderCodeGeneratorDependencies( + [])); + + var providerOptions = new MethodCallCodeFragment(_setProviderOptionMethodInfo); + + var result = codeGenerator.GenerateUseProvider("Server=test;Username=test;Password=test;Database=test", providerOptions); + + Assert.Equal("UseGaussDB", result.Method); + Assert.Collection( + result.Arguments, + a => Assert.Equal("Server=test;Username=test;Password=test;Database=test", a), + a => + { + var nestedClosure = Assert.IsType(a); + + Assert.Equal("x", nestedClosure.Parameter); + Assert.Same(providerOptions, nestedClosure.MethodCalls[0]); + }); + Assert.Null(result.ChainedCall); + } + + [ConditionalFact] + public virtual void Use_provider_method_is_generated_correctly_with_NetTopologySuite() + { + var codeGenerator = new GaussDBCodeGenerator( + new ProviderCodeGeneratorDependencies( + [new GaussDBNetTopologySuiteCodeGeneratorPlugin()])); + + var result = ((IProviderConfigurationCodeGenerator)codeGenerator).GenerateUseProvider("Data Source=Test"); + + Assert.Equal("UseGaussDB", result.Method); + Assert.Collection( + result.Arguments, + a => Assert.Equal("Data Source=Test", a), + a => + { + var nestedClosure = Assert.IsType(a); + + Assert.Equal("x", nestedClosure.Parameter); + Assert.Equal("UseNetTopologySuite", nestedClosure.MethodCalls[0].Method); + }); + Assert.Null(result.ChainedCall); + } + + [ConditionalFact] + public virtual void Use_provider_method_is_generated_correctly_with_NodaTime() + { + var codeGenerator = new GaussDBCodeGenerator( + new ProviderCodeGeneratorDependencies( + [new GaussDBNodaTimeCodeGeneratorPlugin()])); + + var result = ((IProviderConfigurationCodeGenerator)codeGenerator).GenerateUseProvider("Data Source=Test"); + + Assert.Equal("UseGaussDB", result.Method); + Assert.Collection( + result.Arguments, + a => Assert.Equal("Data Source=Test", a), + a => + { + var nestedClosure = Assert.IsType(a); + + Assert.Equal("x", nestedClosure.Parameter); + Assert.Equal("UseNodaTime", nestedClosure.MethodCalls[0].Method); + }); + Assert.Null(result.ChainedCall); + } + + private static readonly MethodInfo _setProviderOptionMethodInfo + = typeof(GaussDBCodeGeneratorTest).GetRuntimeMethod(nameof(SetProviderOption), [typeof(DbContextOptionsBuilder)]); + + public static GaussDBDbContextOptionsBuilder SetProviderOption(DbContextOptionsBuilder optionsBuilder) + => throw new NotSupportedException(); +} diff --git a/test/EFCore.GaussDB.Tests/Storage/GaussDBArrayValueConverterTest.cs b/test/EFCore.GaussDB.Tests/Storage/GaussDBArrayValueConverterTest.cs new file mode 100644 index 0000000000..1105adc613 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Storage/GaussDBArrayValueConverterTest.cs @@ -0,0 +1,73 @@ +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.ValueConversion; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage; + +public class GaussDBArrayValueConverterTest +{ + private static readonly ValueConverter EnumArrayToNumberArray + = new GaussDBArrayConverter(new EnumToNumberConverter()); + + [ConditionalFact] + public void Can_convert_enum_arrays_to_number_arrays() + { + var converter = EnumArrayToNumberArray.ConvertToProviderExpression.Compile(); + + 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)); + } + + [ConditionalFact] + public void Can_convert_enum_arrays_to_number_arrays_object() + { + var converter = EnumArrayToNumberArray.ConvertToProvider; + + 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.Null(converter(null)); + } + + [ConditionalFact] + public void Can_convert_number_arrays_to_enum_arrays() + { + var converter = EnumArrayToNumberArray.ConvertFromProviderExpression.Compile(); + + 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)); + } + + [ConditionalFact] + public void Can_convert_number_arrays_to_enum_arrays_object() + { + var converter = EnumArrayToNumberArray.ConvertFromProvider; + + 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.Null(converter(null)); + } + + private enum Beatles + { + John = 7, + Paul = 4, + George = 1, + Ringo = -1 + } +} diff --git a/test/EFCore.GaussDB.Tests/Storage/GaussDBNodaTimeTypeMappingTest.cs b/test/EFCore.GaussDB.Tests/Storage/GaussDBNodaTimeTypeMappingTest.cs new file mode 100644 index 0000000000..6e1585b122 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Storage/GaussDBNodaTimeTypeMappingTest.cs @@ -0,0 +1,867 @@ +using System.Text; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; +using NodaTime; +using NodaTime.Calendars; +using NodaTime.Text; +using NodaTime.TimeZones; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage; + +public class GaussDBNodaTimeTypeMappingTest +{ + #region Timestamp without time zone + + [Fact] + public void Timestamp_maps_to_LocalDateTime_by_default() + { + Assert.Equal("timestamp without time zone", GetMapping(typeof(LocalDateTime)).StoreType); + Assert.Same(typeof(LocalDateTime), GetMapping("timestamp without time zone").ClrType); + } + + // Mapping Instant to timestamp should only be possible in legacy mode. + // However, when upgrading to 6.0 with existing 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. + [Fact] + public void Instant_maps_to_timestamp_legacy() + { + var mapping = GetMapping(typeof(Instant), "timestamp"); + Assert.Same(typeof(Instant), mapping.ClrType); + Assert.Equal("timestamp", mapping.StoreType); + } + + [Fact] + public void Instant_with_precision() + => Assert.Equal( + "timestamp(3) with time zone", + Mapper.FindMapping(typeof(Instant), "timestamp with time zone", precision: 3)!.StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_LocalDateTime_literal() + { + var mapping = GetMapping(typeof(LocalDateTime)); + Assert.Equal("timestamp without time zone", mapping.StoreType); + + var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660); + Assert.Equal("TIMESTAMP '2018-04-20T10:31:33.666666'", mapping.GenerateSqlLiteral(localDateTime)); + } + + [Fact] + public void GenerateCodeLiteral_returns_LocalDateTime_literal() + { + Assert.Equal("new NodaTime.LocalDateTime(2018, 4, 20, 10, 31)", CodeLiteral(new LocalDateTime(2018, 4, 20, 10, 31))); + Assert.Equal("new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33)", CodeLiteral(new LocalDateTime(2018, 4, 20, 10, 31, 33))); + + var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33) + Period.FromNanoseconds(1); + Assert.Equal("new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33).PlusNanoseconds(1L)", CodeLiteral(localDateTime)); + } + + [Fact] + public void GenerateSqlLiteral_returns_LocalDateTime_infinity_literal() + { + var mapping = GetMapping(typeof(LocalDateTime)); + Assert.Equal(typeof(LocalDateTime), mapping.ClrType); + Assert.Equal("timestamp without time zone", mapping.StoreType); + + // TODO: Switch to use LocalDateTime.MinMaxValue when available (#4061) + Assert.Equal("TIMESTAMP '-infinity'", mapping.GenerateSqlLiteral(LocalDate.MinIsoValue + LocalTime.MinValue)); + Assert.Equal("TIMESTAMP 'infinity'", mapping.GenerateSqlLiteral(LocalDate.MaxIsoValue + LocalTime.MaxValue)); + } + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00")] + [InlineData("9999-12-31T23:59:59.9999999")] + [InlineData("2023-05-29T10:52:47.2064353")] + public void LocalDateTime_json(string dateString) + { + var readerWriter = GetMapping(typeof(LocalDateTime)).JsonValueReaderWriter!; + + var date = LocalDateTimePattern.ExtendedIso.Parse(dateString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(date)[1..^1]; + Assert.Equal(dateString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{dateString}\"")), null); + readerManager.MoveNext(); + var actualDate = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(date, actualDate); + } + + [Fact] + public void GaussDBRange_of_LocalDateTime_is_properly_mapped() + { + Assert.Equal("tsrange", GetMapping(typeof(GaussDBRange)).StoreType); + Assert.Same(typeof(GaussDBRange), GetMapping("tsrange").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_tsrange_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange)); + Assert.Equal("tsrange", mapping.StoreType); + Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange(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)); + } + + [Fact] + public void Array_of_GaussDBRange_of_LocalDateTime_is_properly_mapped() + { + Assert.Equal("tsmultirange", GetMapping(typeof(GaussDBRange[])).StoreType); + Assert.Same(typeof(List>), GetMapping("tsmultirange").ClrType); + } + + [Fact] + public void List_of_GaussDBRange_of_LocalDateTime_is_properly_mapped() + => Assert.Equal("tsmultirange", GetMapping(typeof(List>)).StoreType); + + #endregion Timestamp without time zone + + #region Timestamp with time zone + + [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_timestamptz_Instant_literal() + { + var mapping = GetMapping(typeof(Instant)); + Assert.Equal(typeof(Instant), mapping.ClrType); + Assert.Equal("timestamp with time zone", mapping.StoreType); + + var instant = (new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660)).InUtc().ToInstant(); + Assert.Equal("TIMESTAMPTZ '2018-04-20T10:31:33.666666Z'", mapping.GenerateSqlLiteral(instant)); + } + + [Fact] + public void GenerateSqlLiteral_returns_timestamptz_Instant_infinity_literal() + { + var mapping = GetMapping(typeof(Instant)); + Assert.Equal(typeof(Instant), mapping.ClrType); + Assert.Equal("timestamp with time zone", mapping.StoreType); + + Assert.Equal("TIMESTAMPTZ '-infinity'", mapping.GenerateSqlLiteral(Instant.MinValue)); + Assert.Equal("TIMESTAMPTZ 'infinity'", mapping.GenerateSqlLiteral(Instant.MaxValue)); + } + + [Fact] + public void GenerateSqlLiteral_returns_ZonedDateTime_literal() + { + var mapping = GetMapping(typeof(ZonedDateTime)); + Assert.Equal("timestamp with time zone", mapping.StoreType); + + var zonedDateTime = (new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660)) + .InZone(DateTimeZone.ForOffset(Offset.FromHours(2)), Resolvers.LenientResolver); + Assert.Equal("TIMESTAMPTZ '2018-04-20T10:31:33.666666+02'", mapping.GenerateSqlLiteral(zonedDateTime)); + } + + [Fact] + 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"))""", + CodeLiteral(zonedDateTime)); + } + + [Fact] + public void GenerateSqlLiteral_returns_OffsetDate_time_literal() + { + var mapping = GetMapping(typeof(OffsetDateTime)); + Assert.Equal("timestamp with time zone", mapping.StoreType); + + var offsetDateTime = new OffsetDateTime( + new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660), + Offset.FromHours(2)); + Assert.Equal("TIMESTAMPTZ '2018-04-20T10:31:33.666666+02'", mapping.GenerateSqlLiteral(offsetDateTime)); + } + + [Fact] + public void GenerateCodeLiteral_returns_Instant_literal() + => Assert.Equal( + "NodaTime.Instant.FromUnixTimeTicks(15832607590000000L)", + CodeLiteral(Instant.FromUtc(2020, 3, 3, 18, 39, 19))); + + [Fact] + public void GenerateCodeLiteral_returns_OffsetDate_time_literal() + { + Assert.Equal( + "new NodaTime.OffsetDateTime(new NodaTime.LocalDateTime(2018, 4, 20, 10, 31), NodaTime.Offset.FromHours(-2))", + CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31), Offset.FromHours(-2)))); + + Assert.Equal( + "new NodaTime.OffsetDateTime(new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33), NodaTime.Offset.FromSeconds(9000))", + CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33), Offset.FromHoursAndMinutes(2, 30)))); + + Assert.Equal( + "new NodaTime.OffsetDateTime(new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33), NodaTime.Offset.FromSeconds(-1))", + CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33), Offset.FromSeconds(-1)))); + } + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00Z")] + [InlineData("2023-05-29T10:52:47.2064353Z")] + [InlineData("-0005-05-05T05:55:55.555Z")] + public void Instant_json(string instantString) + { + var readerWriter = GetMapping(typeof(Instant)).JsonValueReaderWriter!; + + var date = InstantPattern.ExtendedIso.Parse(instantString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(date)[1..^1]; + Assert.Equal(instantString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{instantString}\"")), null); + readerManager.MoveNext(); + var actualInstant = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(date, actualInstant); + } + + [ConditionalFact] + public void Instant_json_infinity() + { + var readerWriter = GetMapping(typeof(Instant)).JsonValueReaderWriter!; + + Assert.Equal("infinity", readerWriter.ToJsonString(Instant.MaxValue)[1..^1]); + Assert.Equal("-infinity", readerWriter.ToJsonString(Instant.MinValue)[1..^1]); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(Instant.MaxValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + + readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"-infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(Instant.MinValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + } + + [Fact] + public void Interval_is_properly_mapped() + { + Assert.Equal("tstzrange", GetMapping(typeof(Interval)).StoreType); + Assert.Same(typeof(Interval), GetMapping("tstzrange").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_Interval_literal() + { + var mapping = (IntervalRangeMapping)GetMapping("tstzrange"); + + var value = new Interval( + 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)); + } + + [Fact] + public void GenerateCodeLiteral_returns_tstzrange_Interval_literal() + { + Assert.Equal( + "new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), NodaTime.Instant.FromUnixTimeTicks(15782256000000000L))", + CodeLiteral( + new Interval( + new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 01, 05, 12, 0, 0).InUtc().ToInstant()))); + + Assert.Equal( + "new NodaTime.Interval((NodaTime.Instant?)NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), null)", + CodeLiteral(new Interval(new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), null))); + } + + [Fact] + public void Interval_array_is_properly_mapped() + { + Assert.Equal("tstzmultirange", GetMapping(typeof(Interval[])).StoreType); + Assert.Same(typeof(Interval[]), GetMapping("tstzmultirange").ClrType); + } + + [Fact] + public void Interval_list_is_properly_mapped() + => Assert.Equal("tstzmultirange", GetMapping(typeof(List)).StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_Interval_array_literal() + { + var mapping = GetMapping(typeof(Interval[])); + + var interval = new Interval[] + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + }; + + Assert.Equal( + "'{[1998-04-12T13:26:38Z,1998-04-12T15:26:38Z), [1998-04-13T13:26:38Z,1998-04-13T15:26:38Z)}'::tstzmultirange", + mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateSqlLiteral_returns_Interval_list_literal() + { + var mapping = GetMapping(typeof(List)); + + var interval = new List + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + }; + + Assert.Equal( + "'{[1998-04-12T13:26:38Z,1998-04-12T15:26:38Z), [1998-04-13T13:26:38Z,1998-04-13T15:26:38Z)}'::tstzmultirange", + mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateCodeLiteral_returns_Interval_array_literal() + => Assert.Equal( + "new[] { new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8923875980000000L), NodaTime.Instant.FromUnixTimeTicks(8923947980000000L)), new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8924739980000000L), NodaTime.Instant.FromUnixTimeTicks(8924811980000000L)) }", + CodeLiteral( + new Interval[] + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + })); + + [Fact] + public void GenerateCodeLiteral_returns_Interval_list_literal() + => Assert.Equal( + "new List { new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8923875980000000L), NodaTime.Instant.FromUnixTimeTicks(8923947980000000L)), new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8924739980000000L), NodaTime.Instant.FromUnixTimeTicks(8924811980000000L)) }", + CodeLiteral( + new List + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + })); + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_Instant_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange)); + Assert.Equal("tstzrange", mapping.StoreType); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange( + 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)); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_ZonedDateTime_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange)); + Assert.Equal("tstzrange", mapping.StoreType); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange( + 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)); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_OffsetDateTime_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange)); + Assert.Equal("tstzrange", mapping.StoreType); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange( + 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)); + } + + [Fact] + public void Array_of_GaussDBRange_of_Instant_is_properly_mapped() + => Assert.Equal("tstzmultirange", GetMapping(typeof(GaussDBRange[])).StoreType); + + [Fact] + public void List_of_GaussDBRange_of_Instant_is_properly_mapped() + => Assert.Equal("tstzmultirange", GetMapping(typeof(List>)).StoreType); + + #endregion Timestamp with time zone + + #region date/daterange/datemultirange + + [Fact] + public void LocalDate_is_properly_mapped() + { + Assert.Equal("date", GetMapping(typeof(LocalDate)).StoreType); + Assert.Same(typeof(LocalDate), GetMapping("date").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_LocalDate_literal() + { + var mapping = GetMapping(typeof(LocalDate)); + + Assert.Equal("DATE '2018-04-20'", mapping.GenerateSqlLiteral(new LocalDate(2018, 4, 20))); + } + + [Fact] + public void GenerateSqlLiteral_returns_LocalDate_infinity_literal() + { + var mapping = GetMapping(typeof(LocalDate)); + + Assert.Equal("DATE '-infinity'", mapping.GenerateSqlLiteral(LocalDate.MinIsoValue)); + Assert.Equal("DATE 'infinity'", mapping.GenerateSqlLiteral(LocalDate.MaxIsoValue)); + } + + [Fact] + public void GenerateCodeLiteral_returns_LocalDate_literal() + { + Assert.Equal("new NodaTime.LocalDate(2018, 4, 20)", CodeLiteral(new LocalDate(2018, 4, 20))); + Assert.Equal("new NodaTime.LocalDate(-2017, 4, 20)", CodeLiteral(new LocalDate(Era.BeforeCommon, 2018, 4, 20))); + } + + [ConditionalTheory] + [InlineData("0001-01-01")] + [InlineData("2023-05-29")] + [InlineData("-0005-05-05")] + public void LocalDate_json(string dateString) + { + var readerWriter = GetMapping(typeof(LocalDate)).JsonValueReaderWriter!; + + var date = LocalDatePattern.Iso.Parse(dateString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(date)[1..^1]; + Assert.Equal(dateString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{dateString}\"")), null); + readerManager.MoveNext(); + var actualDate = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(date, actualDate); + } + + [ConditionalFact] + public void LocalDate_json_infinity() + { + var readerWriter = GetMapping(typeof(LocalDate)).JsonValueReaderWriter!; + + Assert.Equal("infinity", readerWriter.ToJsonString(LocalDate.MaxIsoValue)[1..^1]); + Assert.Equal("-infinity", readerWriter.ToJsonString(LocalDate.MinIsoValue)[1..^1]); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(LocalDate.MaxIsoValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + + readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"-infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(LocalDate.MinIsoValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + } + + [Fact] + public void DateInterval_is_properly_mapped() + { + Assert.Equal("daterange", GetMapping(typeof(DateInterval)).StoreType); + Assert.Same(typeof(DateInterval), GetMapping("daterange").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_DateInterval_literal() + { + var mapping = GetMapping(typeof(DateInterval)); + Assert.Equal("daterange", mapping.StoreType); + + var interval = new DateInterval(new LocalDate(2020, 01, 01), new LocalDate(2020, 12, 25)); + Assert.Equal("'[2020-01-01,2020-12-25]'::daterange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateCodeLiteral_returns_DateInterval_literal() + => Assert.Equal( + "new NodaTime.DateInterval(new NodaTime.LocalDate(2020, 1, 1), new NodaTime.LocalDate(2020, 12, 25))", + CodeLiteral(new DateInterval(new LocalDate(2020, 01, 01), new LocalDate(2020, 12, 25)))); + + [Fact] + public void DateInterval_array_is_properly_mapped() + { + Assert.Equal("datemultirange", GetMapping(typeof(DateInterval[])).StoreType); + Assert.Same(typeof(DateInterval[]), GetMapping("datemultirange").ClrType); + } + + [Fact] + public void DateInterval_list_is_properly_mapped() + => Assert.Equal("datemultirange", GetMapping(typeof(List)).StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_DateInterval_array_literal() + { + var mapping = GetMapping(typeof(DateInterval[])); + + var interval = new DateInterval[] + { + new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) + }; + + Assert.Equal("'{[2002-03-04,2002-03-05], [2002-03-08,2002-03-10]}'::datemultirange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateSqlLiteral_returns_DateInterval_list_literal() + { + var mapping = GetMapping(typeof(List)); + + var interval = new List + { + new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) + }; + + Assert.Equal("'{[2002-03-04,2002-03-05], [2002-03-08,2002-03-10]}'::datemultirange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateCodeLiteral_returns_DateInterval_array_literal() + => Assert.Equal( + "new[] { new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 4), new NodaTime.LocalDate(2002, 3, 5)), new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 8), new NodaTime.LocalDate(2002, 3, 10)) }", + CodeLiteral( + new DateInterval[] + { + new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), + new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) + })); + + [Fact] + public void GenerateCodeLiteral_returns_DateInterval_list_literal() + => Assert.Equal( + "new List { new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 4), new NodaTime.LocalDate(2002, 3, 5)), new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 8), new NodaTime.LocalDate(2002, 3, 10)) }", + CodeLiteral( + new List + { + new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), + new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) + })); + + [Fact] + public void GaussDBRange_of_LocalDate_is_properly_mapped() + => Assert.Equal("daterange", GetMapping(typeof(GaussDBRange)).StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_daterange_LocalDate_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange)); + var value = new GaussDBRange(new LocalDate(2020, 1, 1), new LocalDate(2020, 1, 2)); + Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void Array_of_GaussDBRange_of_LocalDate_is_properly_mapped() + => Assert.Equal("datemultirange", GetMapping(typeof(GaussDBRange[])).StoreType); + + [Fact] + public void List_of_GaussDBRange_of_LocalDate_is_properly_mapped() + => Assert.Equal("datemultirange", GetMapping(typeof(List>)).StoreType); + + #endregion date/daterange/datemultirange + + #region time + + [Fact] + public void GenerateSqlLiteral_returns_LocalTime_literal() + { + var mapping = GetMapping(typeof(LocalTime)); + + Assert.Equal("TIME '10:31:33'", mapping.GenerateSqlLiteral(new LocalTime(10, 31, 33))); + Assert.Equal("TIME '10:31:33.666'", mapping.GenerateSqlLiteral(new LocalTime(10, 31, 33, 666))); + Assert.Equal("TIME '10:31:33.666666'", mapping.GenerateSqlLiteral(new LocalTime(10, 31, 33, 666) + Period.FromTicks(6660))); + } + + [Fact] + public void GenerateCodeLiteral_returns_LocalTime_literal() + { + Assert.Equal("new NodaTime.LocalTime(9, 30)", CodeLiteral(new LocalTime(9, 30))); + Assert.Equal("new NodaTime.LocalTime(9, 30, 15)", CodeLiteral(new LocalTime(9, 30, 15))); + Assert.Equal( + "NodaTime.LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 500000000L)", CodeLiteral(new LocalTime(9, 30, 15, 500))); + Assert.Equal( + "NodaTime.LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 1L)", + CodeLiteral(LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 1))); + } + + [Fact] + public void LocalTime_array_is_properly_mapped() + { + Assert.Equal("time[]", GetMapping(typeof(LocalTime[])).StoreType); + Assert.Same(typeof(List), GetMapping("time[]").ClrType); + } + + [Fact] + public void LocalTime_list_is_properly_mapped() + => Assert.Equal("time[]", GetMapping(typeof(List)).StoreType); + + [ConditionalTheory] + [InlineData("00:00:00.0000000", "00:00:00")] + [InlineData("23:59:59.9999999", "23:59:59.9999999")] + [InlineData("11:05:12.3456789", "11:05:12.3456789")] + public void LocalTime_json(string timeString, string json) + { + var readerWriter = GetMapping(typeof(LocalTime)).JsonValueReaderWriter!; + + var time = LocalTimePattern.ExtendedIso.Parse(timeString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(time)[1..^1]; + Assert.Equal(json, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); + readerManager.MoveNext(); + var actualTime = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(time, actualTime); + } + + #endregion time + + #region timetz + + [Fact] + public void GenerateSqlLiteral_returns_OffsetTime_literal() + { + var mapping = GetMapping(typeof(OffsetTime)); + + Assert.Equal( + "TIMETZ '10:31:33+02'", mapping.GenerateSqlLiteral( + new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHours(2)))); + Assert.Equal( + "TIMETZ '10:31:33-02:30'", mapping.GenerateSqlLiteral( + new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHoursAndMinutes(-2, -30)))); + Assert.Equal( + "TIMETZ '10:31:33.666666Z'", mapping.GenerateSqlLiteral( + new OffsetTime(new LocalTime(10, 31, 33, 666) + Period.FromTicks(6660), Offset.Zero))); + } + + [Fact] + public void GenerateCodeLiteral_returns_OffsetTime_literal() + => Assert.Equal( + "new NodaTime.OffsetTime(new NodaTime.LocalTime(10, 31, 33), NodaTime.Offset.FromHours(2))", + CodeLiteral(new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHours(2)))); + + [ConditionalTheory] + [InlineData("00:00:00.0000000Z", "00:00:00Z")] + [InlineData("23:59:59.999999Z", "23:59:59.999999Z")] + [InlineData("11:05:12-02", "11:05:12-02")] + public void OffsetTime_json(string timeString, string json) + { + var readerWriter = GetMapping(typeof(OffsetTime)).JsonValueReaderWriter!; + + var timeOffset = OffsetTimePattern.ExtendedIso.Parse(timeString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(timeOffset)[1..^1]; + Assert.Equal(json, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); + readerManager.MoveNext(); + var actualTimeOffset = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(timeOffset, actualTimeOffset); + } + + #endregion timetz + + #region interval + + [Fact] + public void Duration_is_properly_mapped() + => Assert.All( + [GetMapping(typeof(Duration)), GetMapping(typeof(Duration), "interval")], + m => + { + Assert.Equal("interval", m.StoreType); + Assert.Same(typeof(Duration), m.ClrType); + }); + + [Fact] + public void Period_is_properly_mapped() + => Assert.All( + [GetMapping(typeof(Period)), GetMapping(typeof(Period), "interval")], + m => + { + Assert.Equal("interval", m.StoreType); + Assert.Same(typeof(Period), m.ClrType); + }); + + [Fact] + public void GenerateSqlLiteral_returns_Period_literal() + { + var mapping = GetMapping(typeof(Period)); + + var hms = Period.FromHours(4) + Period.FromMinutes(3) + Period.FromSeconds(2); + Assert.Equal("INTERVAL 'PT4H3M2S'", mapping.GenerateSqlLiteral(hms)); + + var withMilliseconds = hms + Period.FromMilliseconds(1); + Assert.Equal("INTERVAL 'PT4H3M2.001S'", mapping.GenerateSqlLiteral(withMilliseconds)); + + var withMicroseconds = hms + Period.FromTicks(6660); + Assert.Equal("INTERVAL 'PT4H3M2.000666S'", mapping.GenerateSqlLiteral(withMicroseconds)); + + var withYearMonthDay = hms + Period.FromYears(2018) + Period.FromMonths(4) + Period.FromDays(20); + Assert.Equal("INTERVAL 'P2018Y4M20DT4H3M2S'", mapping.GenerateSqlLiteral(withYearMonthDay)); + } + + [Fact] + public void GenerateCodeLiteral_returns_Period_literal() + { + Assert.Equal("NodaTime.Period.FromHours(5L)", CodeLiteral(Period.FromHours(5))); + + Assert.Equal( + "NodaTime.Period.FromYears(1) + NodaTime.Period.FromMonths(2) + NodaTime.Period.FromWeeks(3) + " + + "NodaTime.Period.FromDays(4) + NodaTime.Period.FromHours(5L) + NodaTime.Period.FromMinutes(6L) + " + + "NodaTime.Period.FromSeconds(7L) + NodaTime.Period.FromMilliseconds(8L) + NodaTime.Period.FromNanoseconds(9L)", + CodeLiteral( + Period.FromYears(1) + + Period.FromMonths(2) + + Period.FromWeeks(3) + + Period.FromDays(4) + + Period.FromHours(5) + + Period.FromMinutes(6) + + Period.FromSeconds(7) + + Period.FromMilliseconds(8) + + Period.FromNanoseconds(9))); + + Assert.Equal("NodaTime.Period.Zero", CodeLiteral(Period.Zero)); + } + + [Fact] + public void GenerateCodeLiteral_returns_Duration_literal() + { + Assert.Equal("NodaTime.Duration.FromHours(5)", CodeLiteral(Duration.FromHours(5))); + + Assert.Equal( + "NodaTime.Duration.FromDays(4) + NodaTime.Duration.FromHours(5) + NodaTime.Duration.FromMinutes(6L) + " + + "NodaTime.Duration.FromSeconds(7L) + NodaTime.Duration.FromMilliseconds(8L)", + CodeLiteral( + Duration.FromDays(4) + + Duration.FromHours(5) + + Duration.FromMinutes(6) + + Duration.FromSeconds(7) + + Duration.FromMilliseconds(8))); + + Assert.Equal("NodaTime.Duration.Zero", CodeLiteral(Duration.Zero)); + } + + [ConditionalTheory] + [InlineData("-10675199:02:48:05.477580", "-10675199 02:48:05.47758")] + [InlineData("10675199:02:48:05.477580", "10675199 02:48:05.47758")] + [InlineData("00:00:00", "00:00:00")] + [InlineData("12:23:23.801885", "12:23:23.801885")] + public void Duration_json(string durationString, string json) + { + var readerWriter = GetMapping(typeof(Duration)).JsonValueReaderWriter!; + + var duration = durationString.Count(c => c == ':') == 3 // there's a Days component + ? DurationPattern.Roundtrip.Parse(durationString).GetValueOrThrow() + : DurationPattern.JsonRoundtrip.Parse(durationString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(duration)[1..^1]; + Assert.Equal(json, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); + readerManager.MoveNext(); + var actualDuration = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(duration, actualDuration); + } + + [ConditionalTheory] + [InlineData("P2018Y4M20DT4H3M2S")] + public void Period_json(string intervalString) + { + var readerWriter = GetMapping(typeof(Period)).JsonValueReaderWriter!; + + var period = PeriodPattern.NormalizingIso.Parse(intervalString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(period)[1..^1]; + Assert.Equal(intervalString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{intervalString}\"")), null); + readerManager.MoveNext(); + var actualPeriod = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(period, actualPeriod); + } + + + #endregion interval + + #region DateTimeZone + + [Fact] + public void DateTimeZone_is_properly_mapped() + { + var mapping = GetMapping(typeof(DateTimeZone)); + + Assert.Same(typeof(DateTimeZone), mapping.ClrType); + Assert.Equal("text", mapping.StoreType); + } + + [Fact] + public void GenerateSqlLiteral_returns_DateTimeZone_literal() + { + var mapping = GetMapping(typeof(DateTimeZone)); + + Assert.Equal("Europe/Berlin", mapping.GenerateSqlLiteral(DateTimeZoneProviders.Tzdb["Europe/Berlin"])); + } + + [Fact] + public void GenerateCodeLiteral_returns_DateTimezone_literal() + => Assert.Equal( + """NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("Europe/Berlin")""", + CodeLiteral(DateTimeZoneProviders.Tzdb["Europe/Berlin"])); + + #endregion + + #region Support + + private static readonly GaussDBTypeMappingSource Mapper = new( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + []), + new RelationalTypeMappingSourceDependencies( + [ + new GaussDBNodaTimeTypeMappingSourcePlugin( + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies())) + ]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions() + ); + + 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 static readonly CSharpHelper CsHelper = new(Mapper); + + private static string CodeLiteral(object value) + => CsHelper.UnknownLiteral(value); + + #endregion Support +} diff --git a/test/EFCore.GaussDB.Tests/Storage/GaussDBSqlGenerationHelperTest.cs b/test/EFCore.GaussDB.Tests/Storage/GaussDBSqlGenerationHelperTest.cs new file mode 100644 index 0000000000..dba4af1f50 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Storage/GaussDBSqlGenerationHelperTest.cs @@ -0,0 +1,42 @@ +using System.Text; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage; + +public class GaussDBSqlGenerationHelperTest +{ + [Theory] + [InlineData("all_lowercase", null, "all_lowercase", "all_lowercase")] + [InlineData("digit", null, "digit", "digit")] + [InlineData("under_score", null, "under_score", "under_score")] + [InlineData("dollar$", null, "dollar$", "dollar$")] + [InlineData("ALL_CAPS", null, "\"ALL_CAPS\"", "\"ALL_CAPS\"")] + [InlineData("oneCap", null, "\"oneCap\"", "\"oneCap\"")] + [InlineData("0starts_with_digit", null, "\"0starts_with_digit\"", "\"0starts_with_digit\"")] + [InlineData("all_lowercase", "all_lowercase", "all_lowercase.all_lowercase", "all_lowercase")] + [InlineData("all_lowercase", "oneCap", "\"oneCap\".all_lowercase", "all_lowercase")] + [InlineData("oneCap", "all_lowercase", "all_lowercase.\"oneCap\"", "\"oneCap\"")] + [InlineData("CAPS", "CAPS", "\"CAPS\".\"CAPS\"", "\"CAPS\"")] + [InlineData("select", "null", "\"null\".\"select\"", "\"select\"")] + public virtual void DelimitIdentifier_quotes_properly( + string identifier, + string schema, + string outputWithSchema, + string outputWithoutSchema) + { + var helper = CreateSqlGenerationHelper(); + Assert.Equal(outputWithoutSchema, helper.DelimitIdentifier(identifier)); + Assert.Equal(outputWithSchema, helper.DelimitIdentifier(identifier, schema)); + + var sb = new StringBuilder(); + helper.DelimitIdentifier(sb, identifier); + Assert.Equal(outputWithoutSchema, sb.ToString()); + + sb = new StringBuilder(); + helper.DelimitIdentifier(sb, identifier, schema); + Assert.Equal(outputWithSchema, sb.ToString()); + } + + private ISqlGenerationHelper CreateSqlGenerationHelper() + => new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()); +} diff --git a/test/EFCore.GaussDB.Tests/Storage/GaussDBTypeMappingSourceTest.cs b/test/EFCore.GaussDB.Tests/Storage/GaussDBTypeMappingSourceTest.cs new file mode 100644 index 0000000000..797b430b59 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Storage/GaussDBTypeMappingSourceTest.cs @@ -0,0 +1,409 @@ +using Microsoft.EntityFrameworkCore.Storage.Json; +using NetTopologySuite.Geometries; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Infrastructure; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage; + +public class GaussDBTypeMappingSourceTest +{ + [Theory] + [InlineData("integer", typeof(int), null, null, null, false)] + [InlineData("integer[]", typeof(List), null, null, null, false)] + [InlineData("int", typeof(int), null, null, null, false)] + [InlineData("int[]", typeof(List), null, null, null, false)] + [InlineData("numeric", typeof(decimal), null, null, null, false)] + [InlineData("numeric(10,2)", typeof(decimal), null, 10, 2, false)] + [InlineData("text", typeof(string), null, null, null, false)] + [InlineData("TEXT", typeof(string), null, null, null, false)] + [InlineData("character(8)", typeof(string), 8, null, null, true)] + [InlineData("char(8)", typeof(string), 8, null, null, true)] + [InlineData("character(1)", typeof(char), 1, null, null, true)] + [InlineData("char(1)", typeof(char), 1, null, null, true)] + [InlineData("character", typeof(char), null, null, null, true)] + [InlineData("character varying(8)", typeof(string), 8, null, null, false)] + [InlineData("varchar(8)", typeof(string), 8, null, null, false)] + [InlineData("varchar", typeof(string), null, null, null, false)] + [InlineData("timestamp with time zone", typeof(DateTime), null, null, null, false)] + [InlineData("timestamp without time zone", typeof(DateTime), null, null, null, false)] + [InlineData("date", typeof(DateOnly), null, null, null, false)] + [InlineData("time", typeof(TimeOnly), null, null, null, false)] + [InlineData("time without time zone", typeof(TimeOnly), null, null, null, false)] + [InlineData("interval", typeof(TimeSpan), null, null, null, false)] + [InlineData("dummy", typeof(DummyType), null, null, null, false)] + [InlineData("int4range", typeof(GaussDBRange), null, null, null, false)] + [InlineData("floatrange", typeof(GaussDBRange), null, null, null, false)] + [InlineData("dummyrange", typeof(GaussDBRange), null, null, null, false)] + [InlineData("int4multirange", typeof(List>), null, null, null, false)] + [InlineData("geometry", typeof(Geometry), null, null, null, false)] + [InlineData("geometry(Polygon)", typeof(Polygon), null, null, null, false)] + [InlineData("geography(Point, 4326)", typeof(Point), null, null, null, false)] + [InlineData("geometry(pointz, 4326)", typeof(Point), null, null, null, false)] + [InlineData("geography(LineStringZM)", typeof(LineString), null, null, null, false)] + [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); + + Assert.NotNull(mapping); + Assert.Same(type, mapping.ClrType); + Assert.Equal(size, mapping.Size); + Assert.Equal(precision, mapping.Precision); + Assert.Equal(scale, mapping.Scale); + Assert.False(mapping.IsUnicode); + Assert.Equal(fixedLength, mapping.IsFixedLength); + Assert.Equal(typeName, mapping.StoreType); + } + + [Fact] + public void Varchar32() + { + var mapping = CreateTypeMappingSource().FindMapping("varchar(32)"); + Assert.Same(typeof(string), mapping.ClrType); + Assert.Equal("varchar(32)", mapping.StoreType); + Assert.Equal(32, mapping.Size); + } + + [Fact] + public void Varchar32_Array() + { + var mapping = CreateTypeMappingSource().FindMapping("varchar(32)[]"); + + var arrayMapping = Assert.IsAssignableFrom(mapping); + Assert.Same(typeof(List), arrayMapping.ClrType); + Assert.Equal("varchar(32)[]", arrayMapping.StoreType); + Assert.Null(arrayMapping.Size); + + var elementMapping = arrayMapping.ElementTypeMapping; + Assert.Same(typeof(string), elementMapping.ClrType); + Assert.Equal("varchar(32)", elementMapping.StoreType); + Assert.Equal(32, elementMapping.Size); + } + + [Fact] + public void Timestamp_without_time_zone_5() + { + var mapping = CreateTypeMappingSource().FindMapping("timestamp(5) without time zone"); + Assert.Same(typeof(DateTime), mapping.ClrType); + Assert.Equal("timestamp(5) without time zone", mapping.StoreType); + // Precision/Scale not actually exposed on RelationalTypeMapping... + } + + [Fact] + public void Timestamp_without_time_zone_Array_5() + { + var arrayMapping = + Assert.IsAssignableFrom(CreateTypeMappingSource().FindMapping("timestamp(5) without time zone[]")); + Assert.Same(typeof(List), arrayMapping.ClrType); + Assert.Equal("timestamp(5) without time zone[]", arrayMapping.StoreType); + + var elementMapping = arrayMapping.ElementTypeMapping; + Assert.Same(typeof(DateTime), elementMapping.ClrType); + Assert.Equal("timestamp(5) without time zone", elementMapping.StoreType); + } + + [Theory] + [InlineData(typeof(int), "integer")] + [InlineData(typeof(int[]), "integer[]")] + [InlineData(typeof(byte[]), "bytea")] + [InlineData(typeof(DateTime), "timestamp with time zone")] + [InlineData(typeof(DateOnly), "date")] + [InlineData(typeof(TimeOnly), "time without time zone")] + [InlineData(typeof(TimeSpan), "interval")] + [InlineData(typeof(DummyType), "dummy")] + [InlineData(typeof(GaussDBRange), "int4range")] + [InlineData(typeof(GaussDBRange), "floatrange")] + [InlineData(typeof(GaussDBRange), "dummyrange")] + [InlineData(typeof(GaussDBRange[]), "int4multirange")] + [InlineData(typeof(List>), "int4multirange")] + [InlineData(typeof(Geometry), "geometry")] + [InlineData(typeof(Point), "geometry")] + public void By_ClrType(Type clrType, string expectedStoreType) + { + var mapping = CreateTypeMappingSource().FindMapping(clrType); + Assert.Equal(expectedStoreType, mapping.StoreType); + Assert.Same(clrType, mapping.ClrType); + } + + [Theory] + [InlineData(typeof(decimal), "numeric(5)")] + [InlineData(typeof(DateTime), "timestamp(5) with time zone")] + [InlineData(typeof(TimeSpan), "interval(5)")] + [InlineData(typeof(int), "integer")] + public void By_ClrType_and_precision(Type clrType, string expectedStoreType) + { + var mapping = CreateTypeMappingSource().FindMapping(clrType, null, precision: 5); + Assert.Equal(expectedStoreType, mapping.StoreType); + Assert.Same(clrType, mapping.ClrType); + } + + [Theory] + [InlineData(typeof(decimal[]), "numeric(5)[]")] + [InlineData(typeof(DateTime[]), "timestamp(5) with time zone[]")] + [InlineData(typeof(TimeSpan[]), "interval(5)[]")] + [InlineData(typeof(int[]), "integer[]")] + public void By_ClrType_and_element_precision(Type clrType, string expectedStoreType) + { + var model = CreateEmptyModel(); + var arrayMapping = CreateTypeMappingSource().FindMapping( + clrType, model, + CreateTypeMappingSource().FindMapping(clrType.GetElementType()!, null, precision: 5)!); + + Assert.Equal(expectedStoreType, arrayMapping.StoreType); + Assert.Same(clrType, arrayMapping.ClrType); + Assert.Null(arrayMapping.Precision); + + var elementMapping = Assert.IsAssignableFrom(arrayMapping.ElementTypeMapping); + Assert.Equal(5, elementMapping.Precision); + Assert.Equal(expectedStoreType[..^2], elementMapping.StoreType); + Assert.Same(clrType.GetElementType(), elementMapping.ClrType); + } + + [Theory] + [InlineData("integer", typeof(int))] + [InlineData("numeric", typeof(float))] + [InlineData("numeric", typeof(double))] + [InlineData("date", typeof(DateOnly))] + [InlineData("date", typeof(DateTime))] + [InlineData("time", typeof(TimeOnly))] + [InlineData("time", typeof(TimeSpan))] + [InlineData("integer[]", typeof(int[]))] + [InlineData("integer[]", typeof(List))] + [InlineData("smallint[]", typeof(byte[]))] + [InlineData("dummy", typeof(DummyType))] + [InlineData("int4range", typeof(GaussDBRange))] + [InlineData("floatrange", typeof(GaussDBRange))] + [InlineData("dummyrange", typeof(GaussDBRange))] + [InlineData("geometry", typeof(Geometry))] + [InlineData("geometry(Point, 4326)", typeof(Geometry))] + [InlineData("xid", typeof(uint))] + [InlineData("xid8", typeof(ulong))] + public void By_StoreType_with_ClrType(string storeType, Type clrType) + { + var mapping = CreateTypeMappingSource().FindMapping(clrType, storeType); + Assert.Equal(storeType, mapping.StoreType); + Assert.Same(clrType, mapping.ClrType); + } + + [Theory] + [InlineData("integer", typeof(UnknownType))] + //[InlineData("integer[]", typeof(UnknownType))] TODO Implement + [InlineData("dummy", typeof(UnknownType))] + [InlineData("int4range", typeof(UnknownType))] + [InlineData("floatrange", typeof(UnknownType))] + [InlineData("dummyrange", typeof(UnknownType))] + [InlineData("geometry", typeof(UnknownType))] + public void By_StoreType_with_wrong_ClrType(string storeType, Type wrongClrType) + => Assert.Null(CreateTypeMappingSource().FindMapping(wrongClrType, storeType)); + + // Happens when using domain/aliases: we don't know about the domain but continue with the mapping based on the ClrType + [Fact] + public void Unknown_StoreType_with_known_ClrType() + => Assert.Equal("some_domain", CreateTypeMappingSource().FindMapping(typeof(int), "some_domain").StoreType); + + [Fact] + public void Varchar_mapping_sets_GaussDBDbType() + { + var mapping = CreateTypeMappingSource().FindMapping("character varying"); + var parameter = (GaussDBParameter)mapping.CreateParameter(new GaussDBCommand(), "p", "foo"); + Assert.Equal(GaussDBDbType.Varchar, parameter.GaussDBDbType); + } + + [Fact] + public void Single_char_mapping_sets_GaussDBDbType() + { + var mapping = CreateTypeMappingSource().FindMapping(typeof(char)); + var parameter = (GaussDBParameter)mapping.CreateParameter(new GaussDBCommand(), "p", "foo"); + Assert.Equal(GaussDBDbType.Char, parameter.GaussDBDbType); + } + + [Fact] + public void String_as_single_char_mapping_sets_GaussDBDbType() + { + var mapping = CreateTypeMappingSource().FindMapping(typeof(string), "char(1)"); + var parameter = (GaussDBParameter)mapping.CreateParameter(new GaussDBCommand(), "p", "foo"); + Assert.Equal(GaussDBDbType.Char, parameter.GaussDBDbType); + } + + [Fact] + public void Array_over_type_mapping_with_value_converter_by_clr_type_array() + => Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping(typeof(LTree[])), typeof(LTree[])); + + [Fact] + public void Array_over_type_mapping_with_value_converter_by_clr_type_list() + => Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping(typeof(List)), typeof(List)); + + [Fact] + public void Array_over_type_mapping_with_value_converter_by_store_type() + => Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping("ltree[]"), typeof(List)); + + private void Array_over_type_mapping_with_value_converter(CoreTypeMapping mapping, Type expectedType) + { + var arrayMapping = (GaussDBArrayTypeMapping)mapping; + Assert.Equal("ltree[]", arrayMapping.StoreType); + Assert.Same(expectedType, arrayMapping.ClrType); + + var elementMapping = arrayMapping.ElementTypeMapping; + Assert.NotNull(elementMapping); + Assert.Equal("ltree", elementMapping.StoreType); + Assert.Same(typeof(LTree), elementMapping.ClrType); + + var arrayConverter = arrayMapping.Converter; + Assert.NotNull(arrayConverter); + Assert.Same(expectedType, arrayConverter.ModelClrType); + Assert.Same(typeof(string[]), arrayConverter.ProviderClrType); + + Assert.Collection( + (ICollection)arrayConverter.ConvertToProvider( + expectedType.IsArray + ? new LTree[] { new("foo"), new("bar") } + : new List { new("foo"), new("bar") }), + s => Assert.Equal("foo", s), + s => Assert.Equal("bar", s)); + } + + [Fact] + public void Multirange_by_clr_type_across_pg_versions() + { + var mapping14 = CreateTypeMappingSource(postgresVersion: new Version(14, 0)).FindMapping(typeof(GaussDBRange[]))!; + var mapping13 = CreateTypeMappingSource(postgresVersion: new Version(13, 0)).FindMapping(typeof(GaussDBRange[]))!; + var mappingDefault = CreateTypeMappingSource().FindMapping(typeof(GaussDBRange[]))!; + + Assert.Equal("int4multirange", mapping14.StoreType); + Assert.Equal("int4range[]", mapping13.StoreType); + + // See #2351 - we didn't put multiranges behind a version opt-in in 6.0, although the default PG version is still 12; this causes + // anyone with arrays of ranges to fail if they upgrade to 6.0 with pre-14 PG. + // Changing this in a patch would break people already using 6.0 with PG14, so multiranges are on by default unless users explicitly + // specify < 14. + // Once 14 is made the default version, this stuff can be removed. + Assert.Equal("int4multirange", mappingDefault.StoreType); + } + + [Fact] + public void Multirange_by_store_type_across_pg_versions() + { + var mapping14 = CreateTypeMappingSource(postgresVersion: new Version(14, 0)).FindMapping("int4multirange")!; + var mapping13 = CreateTypeMappingSource(postgresVersion: new Version(13, 0)).FindMapping("int4multirange"); + var mappingDefault = CreateTypeMappingSource().FindMapping("int4multirange")!; + + Assert.Same(typeof(List>), mapping14.ClrType); + Assert.Null(mapping13); + + // See #2351 - we didn't put multiranges behind a version opt-in in 6.0, although the default PG version is still 12; this causes + // anyone with arrays of ranges to fail if they upgrade to 6.0 with pre-14 PG. + // Changing this in a patch would break people already using 6.0 with PG14, so multiranges are on by default unless users explicitly + // specify < 14. + // Once 14 is made the default version, this stuff can be removed. + 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) + { + GaussDBTypeMappingSource.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 GaussDBTypeMappingSource CreateTypeMappingSource(Version postgresVersion = null) + { + var builder = new DbContextOptionsBuilder(); + var npgsqlBuilder = new GaussDBDbContextOptionsBuilder(builder); + + npgsqlBuilder.MapRange("floatrange"); + npgsqlBuilder.MapRange("dummyrange", subtypeName: "dummy"); + npgsqlBuilder.SetPostgresVersion(postgresVersion); + + var options = new GaussDBSingletonOptions(); + options.Initialize(builder.Options); + + return new GaussDBTypeMappingSource( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + []), + new RelationalTypeMappingSourceDependencies( + [ + new GaussDBNetTopologySuiteTypeMappingSourcePlugin(new GaussDBNetTopologySuiteSingletonOptions()), + new DummyTypeMappingSourcePlugin() + ]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + options); + } + + private class DummyTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin + { + public RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) + => mappingInfo.StoreTypeName is not null + ? mappingInfo.StoreTypeName == "dummy" && (mappingInfo.ClrType is null || mappingInfo.ClrType == typeof(DummyType)) + ? _dummyMapping + : null + : mappingInfo.ClrType == typeof(DummyType) + ? _dummyMapping + : null; + + private readonly DummyMapping _dummyMapping = new(); + + private class DummyMapping : RelationalTypeMapping + { + // TODO: The DbType is a hack, we currently require of range subtype mapping that they other expose an GaussDBDbType + // or a DbType (from which GaussDBDbType is computed), since RangeTypeMapping sends an GaussDBDbType. + // This means we currently don't support ranges over types without GaussDBDbType, which are accessible via + // GaussDBParameter.DataTypeName + public DummyMapping() + : base("dummy", typeof(DummyType), System.Data.DbType.Guid) + { + } + + private DummyMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new DummyMapping(parameters); + } + } + + private class DummyType; + + private class UnknownType; + + protected IModel CreateEmptyModel() + => CreateModelBuilder().Model.FinalizeModel(); + + protected ModelBuilder CreateModelBuilder(Action configureConventions = null) + => GaussDBTestHelpers.Instance.CreateConventionBuilder(configureConventions: configureConventions); + + #endregion Support +} diff --git a/test/EFCore.GaussDB.Tests/Storage/GaussDBTypeMappingTest.cs b/test/EFCore.GaussDB.Tests/Storage/GaussDBTypeMappingTest.cs new file mode 100644 index 0000000000..026eaa0b20 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Storage/GaussDBTypeMappingTest.cs @@ -0,0 +1,1013 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Net; +using System.Net.NetworkInformation; +using System.Numerics; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal.Mapping; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage; + +public class GaussDBTypeMappingTest +{ + #region Numeric + + [Fact] + public void GenerateSqlLiteral_returns_decimal_literal() + { + Assert.Equal( + "1.878787", + GetMapping(typeof(decimal), "numeric").GenerateSqlLiteral(1.878787m)); + + Assert.Equal( + "1.878787", + GetMapping(typeof(float), "numeric").GenerateSqlLiteral(1.878787m)); + + Assert.Equal( + "1.878787", + GetMapping(typeof(double), "numeric").GenerateSqlLiteral(1.878787m)); + } + + #endregion Numeric + + #region Date/Time + + [Fact] + public void DateTime_type_maps_to_timestamptz_by_default() + => Assert.Equal("timestamp with time zone", GetMapping(typeof(DateTime)).StoreType); + + [Fact] + public void Timestamp_maps_to_DateTime_by_default() + => Assert.Same(typeof(DateTime), GetMapping("timestamp without time zone").ClrType); + + [Fact] + public void Timestamptz_maps_to_DateTime_by_default() + => Assert.Same(typeof(DateTime), GetMapping("timestamp with time zone").ClrType); + + [Fact] + public void DateTime_with_precision() + => Assert.Equal( + "timestamp(3) with time zone", + Mapper.FindMapping(typeof(DateTime), "timestamp with time zone", precision: 3)!.StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_date_literal() + { + Assert.Equal( + "DATE '2015-03-12'", + GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(new DateTime(2015, 3, 12))); + + Assert.Equal( + "DATE '2015-03-12'", + GetMapping("date").GenerateSqlLiteral(new DateOnly(2015, 3, 12))); + } + + [Fact] + public void GenerateSqlLiteral_returns_date_infinity_literals() + { + Assert.Equal( + "DATE '-infinity'", + GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(DateTime.MinValue)); + + Assert.Equal( + "DATE 'infinity'", + GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(DateTime.MaxValue)); + + Assert.Equal( + "DATE '-infinity'", + GetMapping("date").GenerateSqlLiteral(DateOnly.MinValue)); + + Assert.Equal( + "DATE 'infinity'", + GetMapping("date").GenerateSqlLiteral(DateOnly.MaxValue)); + } + + [Fact] + public void GenerateSqlLiteral_returns_timestamp_literal() + { + var mapping = GetMapping("timestamp without time zone"); + Assert.Equal( + "TIMESTAMP '1997-12-17T07:37:16'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Local))); + Assert.Equal( + "TIMESTAMP '1997-12-17T07:37:16'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Unspecified))); + Assert.Equal( + "TIMESTAMP '1997-12-17T07:37:16.345'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345))); + } + + [Fact] + public void GenerateSqlLiteral_returns_timestamp_infinity_literals() + { + Assert.Equal( + "TIMESTAMP '-infinity'", + GetMapping("timestamp without time zone").GenerateSqlLiteral(DateTime.MinValue)); + + Assert.Equal( + "TIMESTAMP 'infinity'", + GetMapping("timestamp without time zone").GenerateSqlLiteral(DateTime.MaxValue)); + } + + [Fact] + public void GenerateSqlLiteral_timestamp_does_not_support_utc_datetime() + => Assert.Throws(() => GetMapping("timestamp without time zone").GenerateSqlLiteral(DateTime.UtcNow)); + + [Fact] + public void GenerateSqlLiteral_timestamp_does_not_support_datetimeoffset() + => Assert.Throws( + () => GetMapping("timestamp without time zone").GenerateSqlLiteral(new DateTimeOffset())); + + [Fact] + public void GenerateCodeLiteral_returns_DateTime_utc_literal() + => Assert.Equal( + @"new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Utc)", + CodeLiteral(new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Utc))); + + [Fact] + public void GenerateCodeLiteral_returns_DateTime_local_literal() + => Assert.Equal( + @"new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Local)", + CodeLiteral(new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Local))); + + [Fact] + public void GenerateCodeLiteral_returns_DateTime_unspecified_literal() + => Assert.Equal( + @"new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Unspecified)", + CodeLiteral(new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Unspecified))); + + [Fact] + public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() + { + var mapping = GetMapping("timestamptz"); + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16Z'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Utc))); + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16.345678Z'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345, DateTimeKind.Utc).AddTicks(6780))); + } + + [Fact] + public void GenerateSqlLiteral_returns_timestamptz_infinity_literals() + { + Assert.Equal( + "TIMESTAMPTZ '-infinity'", + GetMapping("timestamptz").GenerateSqlLiteral(DateTime.MinValue)); + + Assert.Equal( + "TIMESTAMPTZ 'infinity'", + GetMapping("timestamptz").GenerateSqlLiteral(DateTime.MaxValue)); + + Assert.Equal( + "TIMESTAMPTZ '-infinity'", + GetMapping(typeof(DateTimeOffset), "timestamptz").GenerateSqlLiteral(DateTimeOffset.MinValue)); + + Assert.Equal( + "TIMESTAMPTZ 'infinity'", + GetMapping(typeof(DateTimeOffset), "timestamptz").GenerateSqlLiteral(DateTimeOffset.MaxValue)); + } + + [Fact] + public void GenerateSqlLiteral_timestamptz_does_not_support_local_datetime() + => Assert.Throws(() => GetMapping("timestamp with time zone").GenerateSqlLiteral(DateTime.Now)); + + [Fact] + public void GenerateSqlLiteral_timestamptz_does_not_support_unspecified_datetime() + => Assert.Throws( + () => GetMapping("timestamp with time zone") + .GenerateSqlLiteral(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified))); + + [Fact] + public void GenerateSqlLiteral_returns_timestamptz_datetimeoffset_literal() + { + var mapping = GetMapping("timestamptz"); + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16+02:00'", + mapping.GenerateSqlLiteral(new DateTimeOffset(1997, 12, 17, 7, 37, 16, TimeSpan.FromHours(2)))); + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16.345+02:00'", + mapping.GenerateSqlLiteral(new DateTimeOffset(1997, 12, 17, 7, 37, 16, 345, TimeSpan.FromHours(2)))); + } + + [Fact] + public void GenerateSqlLiteral_returns_time_literal() + { + var mapping = GetMapping("time"); + + Assert.Equal( + "TIME '04:05:06.123456'", + mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6, 123).Add(TimeSpan.FromTicks(4560)))); + Assert.Equal( + "TIME '04:05:06.000123'", + mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6).Add(TimeSpan.FromTicks(1230)))); + Assert.Equal("TIME '04:05:06.123'", mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6, 123))); + Assert.Equal("TIME '04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(4, 5, 6))); + + Assert.Equal( + "TIME '04:05:06.123456'", + mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6, 123).Add(TimeSpan.FromTicks(4560)))); + Assert.Equal( + "TIME '04:05:06.000123'", + mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6).Add(TimeSpan.FromTicks(1230)))); + Assert.Equal("TIME '04:05:06.123'", mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6, 123))); + Assert.Equal("TIME '04:05:06'", mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6))); + Assert.Equal("TIME '13:05:06'", mapping.GenerateSqlLiteral(new TimeOnly(13, 5, 6))); + } + + [Fact] + public void GenerateSqlLiteral_returns_timetz_literal() + { + var mapping = GetMapping("timetz"); + Assert.Equal( + "TIMETZ '04:05:06.123456+3'", + mapping.GenerateSqlLiteral( + new DateTimeOffset(2015, 3, 12, 4, 5, 6, 123, TimeSpan.FromHours(3)) + .AddTicks(4560))); + Assert.Equal( + "TIMETZ '04:05:06.789+3'", mapping.GenerateSqlLiteral(new DateTimeOffset(2015, 3, 12, 4, 5, 6, 789, TimeSpan.FromHours(3)))); + Assert.Equal("TIMETZ '04:05:06-3'", mapping.GenerateSqlLiteral(new DateTimeOffset(2015, 3, 12, 4, 5, 6, TimeSpan.FromHours(-3)))); + } + + [Fact] + public void GenerateSqlLiteral_returns_interval_literal() + { + var mapping = GetMapping("interval"); + Assert.Equal( + "INTERVAL '3 04:05:06.007008'", mapping.GenerateSqlLiteral( + new TimeSpan(3, 4, 5, 6, 7) + .Add(TimeSpan.FromTicks(80)))); + Assert.Equal("INTERVAL '3 04:05:06.007'", mapping.GenerateSqlLiteral(new TimeSpan(3, 4, 5, 6, 7))); + Assert.Equal("INTERVAL '3 04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(3, 4, 5, 6))); + Assert.Equal("INTERVAL '04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(4, 5, 6))); + Assert.Equal("INTERVAL '-3 04:05:06.007'", mapping.GenerateSqlLiteral(new TimeSpan(-3, -4, -5, -6, -7))); + } + + #endregion Date/Time + + #region Networking + + [Fact] + public void GenerateSqlLiteral_returns_macaddr_literal() + => Assert.Equal("MACADDR '001122334455'", GetMapping("macaddr").GenerateSqlLiteral(PhysicalAddress.Parse("00-11-22-33-44-55"))); + + [Fact] + public void GenerateCodeLiteral_returns_macaddr_literal() + => Assert.Equal( + """System.Net.NetworkInformation.PhysicalAddress.Parse("001122334455")""", + CodeLiteral(PhysicalAddress.Parse("00-11-22-33-44-55"))); + + [Fact] + public void GenerateSqlLiteral_returns_macaddr8_literal() + => Assert.Equal( + "MACADDR8 '0011223344556677'", GetMapping("macaddr8").GenerateSqlLiteral(PhysicalAddress.Parse("00-11-22-33-44-55-66-77"))); + + [Fact] + public void GenerateCodeLiteral_returns_macaddr8_literal() + => Assert.Equal( + """System.Net.NetworkInformation.PhysicalAddress.Parse("0011223344556677")""", + CodeLiteral(PhysicalAddress.Parse("00-11-22-33-44-55-66-77"))); + + [Fact] + public void GenerateSqlLiteral_returns_inet_literal() + => Assert.Equal("INET '192.168.1.1'", GetMapping("inet").GenerateSqlLiteral(IPAddress.Parse("192.168.1.1"))); + + [Fact] + public void GenerateCodeLiteral_returns_inet_literal() + => Assert.Equal("""System.Net.IPAddress.Parse("192.168.1.1")""", CodeLiteral(IPAddress.Parse("192.168.1.1"))); + + [Fact] + public void GenerateSqlLiteral_returns_cidr_literal() + => Assert.Equal("CIDR '192.168.1.0/24'", GetMapping("cidr").GenerateSqlLiteral(new GaussDBCidr(IPAddress.Parse("192.168.1.0"), 24))); + + [Fact] + public void GenerateCodeLiteral_returns_cidr_literal() + => Assert.Equal( + """new GaussDBTypes.GaussDBCidr(System.Net.IPAddress.Parse("192.168.1.0"), (byte)24)""", + CodeLiteral(new GaussDBCidr(IPAddress.Parse("192.168.1.0"), 24))); + + #endregion Networking + + #region Geometric + + [Fact] + public void GenerateSqlLiteral_returns_point_literal() + => Assert.Equal("POINT '(3.5,4.5)'", GetMapping("point").GenerateSqlLiteral(new GaussDBPoint(3.5, 4.5))); + + [Fact] + public void GenerateCodeLiteral_returns_point_literal() + => Assert.Equal("new GaussDBTypes.GaussDBPoint(3.5, 4.5)", CodeLiteral(new GaussDBPoint(3.5, 4.5))); + + [Fact] + public void GenerateSqlLiteral_returns_line_literal() + => Assert.Equal("LINE '{3.5,4.5,10}'", GetMapping("line").GenerateSqlLiteral(new GaussDBLine(3.5, 4.5, 10))); + + [Fact] + public void GenerateCodeLiteral_returns_line_literal() + => Assert.Equal("new GaussDBTypes.GaussDBLine(3.5, 4.5, 10.0)", CodeLiteral(new GaussDBLine(3.5, 4.5, 10))); + + [Fact] + public void GenerateSqlLiteral_returns_lseg_literal() + => Assert.Equal("LSEG '[(3.5,4.5),(5.5,6.5)]'", GetMapping("lseg").GenerateSqlLiteral(new GaussDBLSeg(3.5, 4.5, 5.5, 6.5))); + + [Fact] + public void GenerateCodeLiteral_returns_lseg_literal() + => Assert.Equal("new GaussDBTypes.GaussDBLSeg(3.5, 4.5, 5.5, 6.5)", CodeLiteral(new GaussDBLSeg(3.5, 4.5, 5.5, 6.5))); + + [Fact] + public void GenerateSqlLiteral_returns_box_literal() + => Assert.Equal("BOX '((4,3),(2,1))'", GetMapping("box").GenerateSqlLiteral(new GaussDBBox(1, 2, 3, 4))); + + [Fact] + public void GenerateCodeLiteral_returns_box_literal() + => Assert.Equal("new GaussDBTypes.GaussDBBox(3.0, 4.0, 1.0, 2.0)", CodeLiteral(new GaussDBBox(1, 2, 3, 4))); + + [Fact] + public void GenerateSqlLiteral_returns_path_closed_literal() + => Assert.Equal( + "PATH '((1,2),(3,4))'", GetMapping("path").GenerateSqlLiteral( + new GaussDBPath( + new GaussDBPoint(1, 2), + new GaussDBPoint(3, 4) + ))); + + [Fact] + public void GenerateCodeLiteral_returns_closed_path_literal() + => Assert.Equal( + "new GaussDBTypes.GaussDBPath(new GaussDBPoint[] { new GaussDBTypes.GaussDBPoint(1.0, 2.0), new GaussDBTypes.GaussDBPoint(3.0, 4.0) }, false)", + CodeLiteral( + new GaussDBPath( + new GaussDBPoint(1, 2), + new GaussDBPoint(3, 4) + ))); + + [Fact] + public void GenerateSqlLiteral_returns_path_open_literal() + => Assert.Equal( + "PATH '[(1,2),(3,4)]'", GetMapping("path").GenerateSqlLiteral( + new GaussDBPath( + new GaussDBPoint(1, 2), + new GaussDBPoint(3, 4) + ) { Open = true })); + + [Fact] + public void GenerateCodeLiteral_returns_open_path_literal() + => Assert.Equal( + "new GaussDBTypes.GaussDBPath(new GaussDBPoint[] { new GaussDBTypes.GaussDBPoint(1.0, 2.0), new GaussDBTypes.GaussDBPoint(3.0, 4.0) }, true)", + CodeLiteral( + new GaussDBPath( + new GaussDBPoint(1, 2), + new GaussDBPoint(3, 4) + ) { Open = true })); + + [Fact] + public void GenerateSqlLiteral_returns_polygon_literal() + => Assert.Equal( + "POLYGON '((1,2),(3,4))'", GetMapping("polygon").GenerateSqlLiteral( + new GaussDBPolygon( + new GaussDBPoint(1, 2), + new GaussDBPoint(3, 4) + ))); + + [Fact] + public void GenerateCodeLiteral_returns_polygon_literal() + => Assert.Equal( + "new GaussDBTypes.GaussDBPolygon(new GaussDBPoint[] { new GaussDBTypes.GaussDBPoint(1.0, 2.0), new GaussDBTypes.GaussDBPoint(3.0, 4.0) })", + CodeLiteral( + new GaussDBPolygon( + new GaussDBPoint(1, 2), + new GaussDBPoint(3, 4) + ))); + + [Fact] + public void GenerateSqlLiteral_returns_circle_literal() + => Assert.Equal("CIRCLE '<(3.5,4.5),5.5>'", GetMapping("circle").GenerateSqlLiteral(new GaussDBCircle(3.5, 4.5, 5.5))); + + [Fact] + public void GenerateCodeLiteral_returns_circle_literal() + => Assert.Equal("new GaussDBTypes.GaussDBCircle(3.5, 4.5, 5.5)", CodeLiteral(new GaussDBCircle(3.5, 4.5, 5.5))); + + #endregion Geometric + + #region Misc + + [Fact] + public void GenerateSqlLiteral_returns_bool_literal() + => Assert.Equal("TRUE", GetMapping("bool").GenerateSqlLiteral(true)); + + [Fact] + public void GenerateSqlLiteral_returns_varbit_literal() + => 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([true, false]))); + + [Fact] + public void GenerateSqlLiteral_returns_bit_literal() + => 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([true, false]))); + + [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] + public void ValueComparer_hstore_array() + { + // This exercises array's comparer when the element has its own non-null comparer + var source = new[] { new Dictionary { { "k1", "v1" } }, new Dictionary { { "k2", "v2" } }, }; + + var comparer = GetMapping(typeof(Dictionary[])).Comparer; + var snapshot = (Dictionary[])comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + source[1]["k2"] = "v8"; + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact] + public void GenerateSqlLiteral_returns_bytea_literal() + => Assert.Equal(@"BYTEA E'\\xDEADBEEF'", GetMapping("bytea").GenerateSqlLiteral(new byte[] { 222, 173, 190, 239 })); + + [Fact] + public void GenerateSqlLiteral_returns_hstore_literal() + => Assert.Equal( + """HSTORE '"k1"=>"v1","k2"=>"v2"'""", + GetMapping("hstore").GenerateSqlLiteral(new Dictionary { { "k1", "v1" }, { "k2", "v2" } })); + + [Fact] + public void GenerateSqlLiteral_returns_BigInteger_literal() + { + var mapping = GetMapping(typeof(BigInteger)); + + Assert.Equal(@"0", mapping.GenerateSqlLiteral(BigInteger.Zero)); + Assert.Equal(int.MaxValue.ToString(), mapping.GenerateSqlLiteral(new BigInteger(int.MaxValue))); + Assert.Equal(int.MinValue.ToString(), mapping.GenerateSqlLiteral(new BigInteger(int.MinValue))); + } + + [Fact] + public void GenerateCodeLiteral_returns_BigInteger_literal() + => Assert.Equal( + """BigInteger.Parse("18446744073709551615", NumberFormatInfo.InvariantInfo)""", + CodeLiteral(new BigInteger(ulong.MaxValue))); + + [Fact] + public void ValueComparer_hstore_as_Dictionary() + { + var source = new Dictionary { { "k1", "v1" }, { "k2", "v2" } }; + + var comparer = GetMapping("hstore").Comparer; + var snapshot = (Dictionary)comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + snapshot.Remove("k1"); + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact] + public void ValueComparer_hstore_as_ImmutableDictionary() + { + var source = ImmutableDictionary.Empty + .Add("k1", "v1") + .Add("k2", "v2"); + + var comparer = Mapper.FindMapping(typeof(ImmutableDictionary), "hstore").Comparer; + var snapshot = comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + source = source.Remove("k1"); + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact] + public void GenerateSqlLiteral_returns_enum_literal() + { + var mapping = new GaussDBEnumTypeMapping("dummy_enum", "dummy_enum", typeof(DummyEnum), new Dictionary + { + [DummyEnum.Happy] = "happy", + [DummyEnum.Sad] = "sad" + }); + + Assert.Equal("'sad'::dummy_enum", mapping.GenerateSqlLiteral(DummyEnum.Sad)); + } + + [Fact] + public void GenerateSqlLiteral_returns_enum_uppercase_literal() + { + var mapping = new GaussDBEnumTypeMapping(""" + "DummyEnum" + """, "DummyEnum", typeof(DummyEnum), new Dictionary + { + [DummyEnum.Happy] = "happy", + [DummyEnum.Sad] = "sad" + }); + + Assert.Equal(""" + 'sad'::"DummyEnum" + """, mapping.GenerateSqlLiteral(DummyEnum.Sad)); + } + + private enum DummyEnum + { + // ReSharper disable once UnusedMember.Local + Happy, + Sad + } + + [Fact] + public void GenerateSqlLiteral_returns_tid_literal() + => Assert.Equal(@"TID '(0,1)'", GetMapping("tid").GenerateSqlLiteral(new GaussDBTid(0, 1))); + + [Fact] + public void GenerateSqlLiteral_returns_pg_lsn_literal() + => Assert.Equal(@"PG_LSN '12345/67890'", GetMapping("pg_lsn").GenerateSqlLiteral(GaussDBLogSequenceNumber.Parse("12345/67890"))); + + #endregion Misc + + #region Array + + [Fact] + public void GenerateSqlLiteral_returns_array_literal() + => Assert.Equal("ARRAY[3,4]::integer[]", GetMapping(typeof(int[])).GenerateSqlLiteral(new[] { 3, 4 })); + + [Fact] + public void GenerateSqlLiteral_returns_array_empty_literal() + => Assert.Equal("ARRAY[]::smallint[]", GetMapping(typeof(short[])).GenerateSqlLiteral(Array.Empty())); + + [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] + public void ValueComparer_int_array() + { + // This exercises array's comparer when the element doesn't have a comparer, but it implements + // IEquatable + var source = new[] { 2, 3, 4 }; + + var comparer = GetMapping(typeof(int[])).Comparer; + var snapshot = (int[])comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + snapshot[1] = 8; + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] + public void ValueComparer_int_list() + { + var source = new List + { + 2, + 3, + 4 + }; + + var comparer = GetMapping(typeof(List)).Comparer; + var snapshot = (List)comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + snapshot[1] = 8; + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] + public void ValueComparer_nullable_int_array() + { + var source = new int?[] { 2, 3, 4, null }; + + var comparer = GetMapping(typeof(int?[])).Comparer; + var snapshot = (int?[])comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + snapshot[1] = 8; + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] + public void ValueComparer_nullable_int_list() + { + var source = new List + { + 2, + 3, + 4, + null + }; + + var comparer = GetMapping(typeof(List)).Comparer; + var snapshot = (List)comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + snapshot[1] = 8; + Assert.False(comparer.Equals(source, snapshot)); + } + + [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] + public void ValueComparer_nullable_array_with_iequatable_element() + { + var source = new GaussDBPoint?[] { new GaussDBPoint(1, 1), null }; + + var comparer = GetMapping(typeof(GaussDBPoint?[])).Comparer; + var snapshot = (GaussDBPoint?[])comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + snapshot[1] = new GaussDBPoint(2, 2); + Assert.False(comparer.Equals(source, snapshot)); + } + + #endregion Array + + #region Ranges + + [Fact] + public void GenerateSqlLiteral_returns_range_empty_literal() + { + var value = GaussDBRange.Empty; + var literal = GetMapping("int4range").GenerateSqlLiteral(value); + Assert.Equal("'empty'::int4range", literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_range_empty_literal() + => Assert.Equal("new GaussDBTypes.GaussDBRange(0, false, 0, false)", CodeLiteral(GaussDBRange.Empty)); + + [Fact] + public void GenerateSqlLiteral_returns_range_inclusive_literal() + { + var value = new GaussDBRange(4, 7); + var literal = GetMapping("int4range").GenerateSqlLiteral(value); + Assert.Equal("'[4,7]'::int4range", literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_range_inclusive_literal() + => Assert.Equal("new GaussDBTypes.GaussDBRange(4, 7)", CodeLiteral(new GaussDBRange(4, 7))); + + [Fact] + public void GenerateSqlLiteral_returns_range_inclusive_exclusive_literal() + { + var value = new GaussDBRange(4, false, 7, true); + var literal = GetMapping("int4range").GenerateSqlLiteral(value); + Assert.Equal("'(4,7]'::int4range", literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_range_inclusive_exclusive_literal() + => Assert.Equal("new GaussDBTypes.GaussDBRange(4, false, 7, true)", CodeLiteral(new GaussDBRange(4, false, 7, true))); + + [Fact] + public void GenerateSqlLiteral_returns_range_infinite_literal() + { + var value = new GaussDBRange(0, false, true, 7, true, false); + var literal = GetMapping("int4range").GenerateSqlLiteral(value); + Assert.Equal("'(,7]'::int4range", literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_range_infinite_literal() + => Assert.Equal( + "new GaussDBTypes.GaussDBRange(0, false, true, 7, true, false)", + CodeLiteral(new GaussDBRange(0, false, true, 7, true, false))); + + // Tests for the built-in ranges + + [Fact] + public void GenerateSqlLiteral_returns_int4range_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping("int4range"); + Assert.Equal("integer", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange(4, 7); + Assert.Equal("'[4,7]'::int4range", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateSqlLiteral_returns_int8range_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping("int8range"); + Assert.Equal("bigint", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange(4, 7); + Assert.Equal("'[4,7]'::int8range", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateSqlLiteral_returns_numrange_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping("numrange"); + Assert.Equal("numeric", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange(4, 7); + Assert.Equal("'[4.0,7.0]'::numrange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateSqlLiteral_returns_tsrange_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping("tsrange"); + Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange(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)); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping("tstzrange"); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange( + 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)); + } + + [Fact] + public void GenerateSqlLiteral_returns_daterange_DateOnly_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping("daterange"); + Assert.Equal("date", mapping.SubtypeMapping.StoreType); + Assert.Equal("date", ((GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange))).SubtypeMapping.StoreType); + + var value = new GaussDBRange(new DateOnly(2020, 1, 1), new DateOnly(2020, 1, 2)); + Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateSqlLiteral_returns_daterange_DateTime_literal() + { + var mapping = (GaussDBCidrTypeMapping)GetMapping(typeof(GaussDBRange), "daterange"); + Assert.Equal("date", mapping.SubtypeMapping.StoreType); + + var value = new GaussDBRange(new DateTime(2020, 1, 1), new DateTime(2020, 1, 2)); + Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); + } + + #endregion Ranges + + #region Multiranges + + [Fact] + public void GenerateSqlLiteral_returns_multirange_literal() + { + var value = new GaussDBRange[] + { + new(4, 7), + new(9, lowerBoundIsInclusive: true, 10, upperBoundIsInclusive: false), + new( + 13, lowerBoundIsInclusive: false, lowerBoundInfinite: false, default, upperBoundIsInclusive: false, + upperBoundInfinite: true) + }; + var literal = GetMapping("int4multirange").GenerateSqlLiteral(value); + Assert.Equal("'{[4,7], [9,10), (13,)}'::int4multirange", literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_multirange_array_literal() + { + var value = new GaussDBRange[] + { + new(4, 7), + new(9, lowerBoundIsInclusive: true, 10, upperBoundIsInclusive: false), + new( + 13, lowerBoundIsInclusive: false, lowerBoundInfinite: false, default, upperBoundIsInclusive: false, + upperBoundInfinite: true) + }; + var literal = CodeLiteral(value); + Assert.Equal( + "new[] { new GaussDBTypes.GaussDBRange(4, 7), new GaussDBTypes.GaussDBRange(9, true, 10, false), new GaussDBTypes.GaussDBRange(13, false, false, 0, false, true) }", + literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_multirange_list_literal() + { + var value = new List> + { + new(4, 7), + new(9, lowerBoundIsInclusive: true, 10, upperBoundIsInclusive: false), + new( + 13, lowerBoundIsInclusive: false, lowerBoundInfinite: false, default, upperBoundIsInclusive: false, + upperBoundInfinite: true) + }; + var literal = CodeLiteral(value); + Assert.Equal( + "new List> { new GaussDBTypes.GaussDBRange(4, 7), new GaussDBTypes.GaussDBRange(9, true, 10, false), new GaussDBTypes.GaussDBRange(13, false, false, 0, false, true) }", + literal); + } + + [Fact] + public void GenerateSqlLiteral_returns_multirange_empty_literal() + { + var value = Array.Empty>(); + var literal = GetMapping("int4multirange").GenerateSqlLiteral(value); + Assert.Equal("'{}'::int4multirange", literal); + } + + [Fact] + public void GenerateCodeLiteral_returns_multirange_empty_array_literal() + { + var value = Array.Empty>(); + var literal = CodeLiteral(value); + Assert.Equal("new GaussDBRange[0]", literal); + } + + #endregion Multiranges + + #region Full text search + +#pragma warning disable CS0618 // Full-text search client-parsing is obsolete + [Fact] + public void GenerateSqlLiteral_returns_tsquery_literal() + => Assert.Equal( + @"TSQUERY '''a'' & ''b'''", + GetMapping("tsquery").GenerateSqlLiteral(GaussDBTsQuery.Parse("a & b"))); + + [Fact] + public void GenerateSqlLiteral_returns_tsvector_literal() + => Assert.Equal( + @"TSVECTOR '''a'' ''b'''", + GetMapping("tsvector").GenerateSqlLiteral(GaussDBTsVector.Parse("a b"))); +#pragma warning restore CS0618 + + [Fact] + public void GenerateSqlLiteral_returns_ranking_normalization_literal() + => Assert.Equal( + $"{(int)GaussDBTsRankingNormalization.DivideByLength}", + GetMapping(typeof(GaussDBTsRankingNormalization)) + .GenerateSqlLiteral(GaussDBTsRankingNormalization.DivideByLength)); + + #endregion Full text search + + #region Json + + [Fact] + public void GenerateSqlLiteral_returns_jsonb_string_literal() + => Assert.Equal("""'{"a":1}'""", GetMapping("jsonb").GenerateSqlLiteral("""{"a":1}""")); + + [Fact] + public void GenerateSqlLiteral_returns_json_string_literal() + => Assert.Equal("""'{"a":1}'""", GetMapping("json").GenerateSqlLiteral("""{"a":1}""")); + + [Fact] + public void GenerateSqlLiteral_returns_jsonb_object_literal() + { + var literal = Mapper.FindMapping(typeof(Customer), "jsonb").GenerateSqlLiteral(SampleCustomer); + Assert.Equal( + """'{"Name":"Joe","Age":25,"IsVip":false,"Orders":[{"Price":99.5,"ShippingAddress":"Some address 1","ShippingDate":"2019-10-01T00:00:00"},{"Price":23,"ShippingAddress":"Some address 2","ShippingDate":"2019-10-10T00:00:00"}]}'""", + literal); + } + + [Fact] + public void GenerateSqlLiteral_returns_json_object_literal() + { + var literal = Mapper.FindMapping(typeof(Customer), "json").GenerateSqlLiteral(SampleCustomer); + Assert.Equal( + """'{"Name":"Joe","Age":25,"IsVip":false,"Orders":[{"Price":99.5,"ShippingAddress":"Some address 1","ShippingDate":"2019-10-01T00:00:00"},{"Price":23,"ShippingAddress":"Some address 2","ShippingDate":"2019-10-10T00:00:00"}]}'""", + literal); + } + + [Fact] + public void GenerateSqlLiteral_returns_jsonb_document_literal() + { + var json = """{"Name":"Joe","Age":25}"""; + var literal = Mapper.FindMapping(typeof(JsonDocument), "jsonb").GenerateSqlLiteral(JsonDocument.Parse(json)); + Assert.Equal($"'{json}'", literal); + } + + [Fact] + public void GenerateSqlLiteral_returns_json_document_literal() + { + var json = """{"Name":"Joe","Age":25}"""; + var literal = Mapper.FindMapping(typeof(JsonDocument), "json").GenerateSqlLiteral(JsonDocument.Parse(json)); + Assert.Equal($"'{json}'", literal); + } + + [Fact] + public void GenerateSqlLiteral_returns_jsonb_element_literal() + { + var json = """{"Name":"Joe","Age":25}"""; + var literal = Mapper.FindMapping(typeof(JsonElement), "jsonb").GenerateSqlLiteral(JsonDocument.Parse(json).RootElement); + Assert.Equal($"'{json}'", literal); + } + + [Fact] + public void GenerateSqlLiteral_returns_json_element_literal() + { + var json = """{"Name":"Joe","Age":25}"""; + var literal = Mapper.FindMapping(typeof(JsonElement), "json").GenerateSqlLiteral(JsonDocument.Parse(json).RootElement); + Assert.Equal($"'{json}'", literal); + } + + [Fact] + 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}"""))); + + [Fact] + public void GenerateCodeLiteral_returns_json_element_literal() + => Assert.Equal( + """System.Text.Json.JsonDocument.Parse("{\"Name\":\"Joe\",\"Age\":25}", new System.Text.Json.JsonDocumentOptions()).RootElement""", + CodeLiteral(JsonDocument.Parse(@"{""Name"":""Joe"",""Age"":25}").RootElement)); + + [Fact] + public void ValueComparer_JsonDocument() + { + var json = """{"Name":"Joe","Age":25}"""; + var source = JsonDocument.Parse(json); + + var comparer = GetMapping(typeof(JsonDocument)).Comparer; + var snapshot = (JsonDocument)comparer.Snapshot(source); + Assert.Same(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + } + + [Fact] + public void ValueComparer_JsonElement() + { + var json = """{"Name":"Joe","Age":25}"""; + var source = JsonDocument.Parse(json).RootElement; + + var comparer = GetMapping(typeof(JsonElement)).Comparer; + var snapshot = (JsonElement)comparer.Snapshot(source); + Assert.True(comparer.Equals(source, snapshot)); + Assert.False(comparer.Equals(source, JsonDocument.Parse(json).RootElement)); + } + + private static readonly Customer SampleCustomer = new() + { + Name = "Joe", + Age = 25, + IsVip = false, + Orders = + [ + new Order + { + Price = 99.5m, + ShippingAddress = "Some address 1", + ShippingDate = new DateTime(2019, 10, 1) + }, + new Order + { + Price = 23, + ShippingAddress = "Some address 2", + ShippingDate = new DateTime(2019, 10, 10) + } + ] + }; + + public class Customer + { + public string Name { get; set; } + public int Age { get; set; } + public bool IsVip { get; set; } + public Order[] Orders { get; set; } + } + + public class Order + { + public decimal Price { get; set; } + public string ShippingAddress { get; set; } + public DateTime ShippingDate { get; set; } + } + + #endregion Json + + #region Support + + private static readonly GaussDBTypeMappingSource Mapper = new( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + [] + ), + new RelationalTypeMappingSourceDependencies([]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions() + ); + + 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 static readonly CSharpHelper CsHelper = new(Mapper); + + private static string CodeLiteral(object value) + => CsHelper.UnknownLiteral(value); + + #endregion Support +} diff --git a/test/EFCore.GaussDB.Tests/Storage/LegacyGaussDBTypeMappingTest.cs b/test/EFCore.GaussDB.Tests/Storage/LegacyGaussDBTypeMappingTest.cs new file mode 100644 index 0000000000..f4e67a8074 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Storage/LegacyGaussDBTypeMappingTest.cs @@ -0,0 +1,81 @@ +#if DEBUG + +using Microsoft.EntityFrameworkCore.Storage.Json; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Storage; + +[Collection("LegacyDateTimeTest")] +public class LegacyGaussDBTypeMappingTest : IClassFixture +{ + [Fact] + public void DateTime_type_maps_to_timestamp_by_default() + => Assert.Equal("timestamp without time zone", GetMapping(typeof(DateTime)).StoreType); + + [Fact] + public void Timestamp_maps_to_DateTime_by_default() + => Assert.Same(typeof(DateTime), GetMapping("timestamp without time zone").ClrType); + + [Fact] + public void Timestamptz_maps_to_DateTime_by_default() + => Assert.Same(typeof(DateTime), GetMapping("timestamp with time zone").ClrType); + + [Fact] + public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() + { + var mapping = GetMapping("timestamptz"); + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16Z'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Utc))); + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16Z'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Unspecified))); + + var offset = TimeZoneInfo.Local.BaseUtcOffset; + var offsetStr = (offset < TimeSpan.Zero ? '-' : '+') + offset.ToString(@"hh\:mm"); + Assert.StartsWith( + $"TIMESTAMPTZ '1997-12-17T07:37:16{offsetStr}", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Local))); + + Assert.Equal( + "TIMESTAMPTZ '1997-12-17T07:37:16.345678Z'", + mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345, DateTimeKind.Utc).AddTicks(6780))); + } + + #region Support + + private static readonly GaussDBTypeMappingSource Mapper = new( + new TypeMappingSourceDependencies( + new ValueConverterSelector(new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), + [] + ), + new RelationalTypeMappingSourceDependencies([]), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions()); + + private static RelationalTypeMapping GetMapping(string storeType) + => Mapper.FindMapping(storeType); + + private static RelationalTypeMapping GetMapping(Type clrType) + => Mapper.FindMapping(clrType); + + public class LegacyGaussDBTypeMappingFixture : IDisposable + { + public LegacyGaussDBTypeMappingFixture() + { + GaussDBTypeMappingSource.LegacyTimestampBehavior = true; + } + + public void Dispose() + => GaussDBTypeMappingSource.LegacyTimestampBehavior = false; + } + + #endregion Support +} + +[CollectionDefinition("LegacyDateTimeTest", DisableParallelization = true)] +public class EventSourceTestCollection; + +#endif diff --git a/test/EFCore.PG.Tests/TestUtilities/FakeDiagnosticsLogger.cs b/test/EFCore.GaussDB.Tests/TestUtilities/FakeDiagnosticsLogger.cs similarity index 94% rename from test/EFCore.PG.Tests/TestUtilities/FakeDiagnosticsLogger.cs rename to test/EFCore.GaussDB.Tests/TestUtilities/FakeDiagnosticsLogger.cs index c55131ba99..92bc12051a 100644 --- a/test/EFCore.PG.Tests/TestUtilities/FakeDiagnosticsLogger.cs +++ b/test/EFCore.GaussDB.Tests/TestUtilities/FakeDiagnosticsLogger.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; public class FakeDiagnosticsLogger : IDiagnosticsLogger, ILogger where T : LoggerCategory, new() diff --git a/test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs b/test/EFCore.GaussDB.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs similarity index 94% rename from test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs rename to test/EFCore.GaussDB.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs index 61b17e88e5..5d0e1351ab 100644 --- a/test/EFCore.PG.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs +++ b/test/EFCore.GaussDB.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs @@ -1,4 +1,4 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities.FakeProvider; +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities.FakeProvider; public class FakeRelationalOptionsExtension : RelationalOptionsExtension { diff --git a/test/EFCore.PG.Tests/TestUtilities/FakeRelationalCommandDiagnosticsLogger.cs b/test/EFCore.GaussDB.Tests/TestUtilities/FakeRelationalCommandDiagnosticsLogger.cs similarity index 99% rename from test/EFCore.PG.Tests/TestUtilities/FakeRelationalCommandDiagnosticsLogger.cs rename to test/EFCore.GaussDB.Tests/TestUtilities/FakeRelationalCommandDiagnosticsLogger.cs index c4d96a3167..c21b183cc8 100644 --- a/test/EFCore.PG.Tests/TestUtilities/FakeRelationalCommandDiagnosticsLogger.cs +++ b/test/EFCore.GaussDB.Tests/TestUtilities/FakeRelationalCommandDiagnosticsLogger.cs @@ -1,7 +1,7 @@ #nullable enable using System.Data.Common; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; namespace Microsoft.EntityFrameworkCore.TestUtilities; diff --git a/test/EFCore.GaussDB.Tests/Types/LTreeTest.cs b/test/EFCore.GaussDB.Tests/Types/LTreeTest.cs new file mode 100644 index 0000000000..332c0631f4 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Types/LTreeTest.cs @@ -0,0 +1,8 @@ +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Types; + +public class LTreeTest +{ + [ConditionalFact] + public void ToString_works() + => Assert.Equal("Top.Sub", ((LTree)"Top.Sub").ToString()); +} diff --git a/test/EFCore.GaussDB.Tests/Update/GaussDBModificationCommandBatchFactoryTest.cs b/test/EFCore.GaussDB.Tests/Update/GaussDBModificationCommandBatchFactoryTest.cs new file mode 100644 index 0000000000..9349ab1ce4 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Update/GaussDBModificationCommandBatchFactoryTest.cs @@ -0,0 +1,96 @@ +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.Update.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Update.Internal; + +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Update; + +public class GaussDBModificationCommandBatchFactoryTest +{ + [Fact] + public void Uses_MaxBatchSize_specified_in_GaussDBOptionsExtension() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB("Database=Crunchie", b => b.MaxBatchSize(1)); + + var typeMapper = new GaussDBTypeMappingSource( + TestServiceFactory.Instance.Create(), + TestServiceFactory.Instance.Create(), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions()); + + var factory = new GaussDBModificationCommandBatchFactory( + new ModificationCommandBatchFactoryDependencies( + new RelationalCommandBuilderFactory( + new RelationalCommandBuilderDependencies( + typeMapper, + new ExceptionDetector(), + new LoggingOptions())), + new GaussDBSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()), + new GaussDBUpdateSqlGenerator( + new UpdateSqlGeneratorDependencies( + new GaussDBSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()), + typeMapper)), + new CurrentDbContext(new FakeDbContext()), + new FakeRelationalCommandDiagnosticsLogger(), + new FakeDiagnosticsLogger()), + optionsBuilder.Options); + + var batch = factory.Create(); + + Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); + Assert.False(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); + } + + [Fact] + public void MaxBatchSize_is_optional() + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseGaussDB("Database=Crunchie"); + + var typeMapper = new GaussDBTypeMappingSource( + TestServiceFactory.Instance.Create(), + TestServiceFactory.Instance.Create(), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions()); + + var factory = new GaussDBModificationCommandBatchFactory( + new ModificationCommandBatchFactoryDependencies( + new RelationalCommandBuilderFactory( + new RelationalCommandBuilderDependencies( + typeMapper, + new ExceptionDetector(), + new LoggingOptions())), + new GaussDBSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()), + new GaussDBUpdateSqlGenerator( + new UpdateSqlGeneratorDependencies( + new GaussDBSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()), + typeMapper)), + new CurrentDbContext(new FakeDbContext()), + new FakeRelationalCommandDiagnosticsLogger(), + new FakeDiagnosticsLogger()), + optionsBuilder.Options); + + var batch = factory.Create(); + + Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); + Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); + } + + private class FakeDbContext : DbContext; + + private static INonTrackedModificationCommand CreateModificationCommand( + string name, + string schema, + bool sensitiveLoggingEnabled) + => new ModificationCommandFactory().CreateNonTrackedModificationCommand( + new NonTrackedModificationCommandParameters(name, schema, sensitiveLoggingEnabled)); +} diff --git a/test/EFCore.GaussDB.Tests/Update/GaussDBModificationCommandBatchTest.cs b/test/EFCore.GaussDB.Tests/Update/GaussDBModificationCommandBatchTest.cs new file mode 100644 index 0000000000..03ae075df8 --- /dev/null +++ b/test/EFCore.GaussDB.Tests/Update/GaussDBModificationCommandBatchTest.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.Update.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Storage.Internal; +using HuaweiCloud.EntityFrameworkCore.GaussDB.TestUtilities; +using HuaweiCloud.EntityFrameworkCore.GaussDB.Update.Internal; + +// ReSharper disable once CheckNamespace +namespace HuaweiCloud.EntityFrameworkCore.GaussDB.Tests.Update; + +public class GaussDBModificationCommandBatchTest +{ + [Fact] + public void AddCommand_returns_false_when_max_batch_size_is_reached() + { + var typeMapper = new GaussDBTypeMappingSource( + TestServiceFactory.Instance.Create(), + TestServiceFactory.Instance.Create(), + new GaussDBSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), + new GaussDBSingletonOptions()); + + var batch = new GaussDBModificationCommandBatch( + new ModificationCommandBatchFactoryDependencies( + new RelationalCommandBuilderFactory( + new RelationalCommandBuilderDependencies( + typeMapper, + new ExceptionDetector(), + new LoggingOptions())), + new GaussDBSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()), + new GaussDBUpdateSqlGenerator( + new UpdateSqlGeneratorDependencies( + new GaussDBSqlGenerationHelper( + new RelationalSqlGenerationHelperDependencies()), + typeMapper)), + new CurrentDbContext(new FakeDbContext()), + new FakeRelationalCommandDiagnosticsLogger(), + new FakeDiagnosticsLogger()), + maxBatchSize: 1); + + Assert.True( + batch.TryAddCommand( + CreateModificationCommand("T1", null, false))); + Assert.False( + batch.TryAddCommand( + CreateModificationCommand("T1", null, false))); + } + + private class FakeDbContext : DbContext; + + private static INonTrackedModificationCommand CreateModificationCommand( + string name, + string schema, + bool sensitiveLoggingEnabled) + => new ModificationCommandFactory().CreateNonTrackedModificationCommand( + new NonTrackedModificationCommandParameters(name, schema, sensitiveLoggingEnabled)); +} diff --git a/test/EFCore.PG.FunctionalTests/BadDataJsonDeserializationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BadDataJsonDeserializationNpgsqlTest.cs deleted file mode 100644 index eacc8e7243..0000000000 --- a/test/EFCore.PG.FunctionalTests/BadDataJsonDeserializationNpgsqlTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class BadDataJsonDeserializationNpgsqlTest : BadDataJsonDeserializationTestBase -{ - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => base.OnConfiguring(optionsBuilder.UseNpgsql(b => b.UseNetTopologySuite())); -} diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs deleted file mode 100644 index 3154ab1a18..0000000000 --- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs +++ /dev/null @@ -1,1481 +0,0 @@ -using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations.Schema; -using System.Net.NetworkInformation; - -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -public class BuiltInDataTypesNpgsqlTest : BuiltInDataTypesTestBase -{ - // ReSharper disable once UnusedParameter.Local - public BuiltInDataTypesNpgsqlTest(BuiltInDataTypesNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [Fact] - public void Sql_translation_uses_type_mapper_when_constant() - { - using var context = CreateContext(); - var results = context.Set() - .Where(e => e.TimeSpanAsTime == new TimeSpan(0, 1, 2)) - .Select(e => e.Int) - .ToList(); - - Assert.Empty(results); - - AssertSql( - """ -SELECT m."Int" -FROM "MappedNullableDataTypes" AS m -WHERE m."TimeSpanAsTime" = TIME '00:01:02' -"""); - } - - [Fact] - public void Sql_translation_uses_type_mapper_when_parameter() - { - using var context = CreateContext(); - var timeSpan = new TimeSpan(2, 1, 0); - var results = context.Set() - .Where(e => e.TimeSpanAsTime == timeSpan) - .Select(e => e.Int) - .ToList(); - - Assert.Empty(results); - - AssertSql( - """ -@timeSpan='02:01:00' (Nullable = true) - -SELECT m."Int" -FROM "MappedNullableDataTypes" AS m -WHERE m."TimeSpanAsTime" = @timeSpan -"""); - } - - [Fact] - public virtual void Can_query_using_any_mapped_data_type() - { - using (var context = CreateContext()) - { - context.Set().Add( - new MappedNullableDataTypes - { - Int = 999, - LongAsBigint = 78L, - ShortAsSmallint = 79, - ByteAsSmallint = 80, - UintAsInt = uint.MaxValue, - UlongAsBigint = ulong.MaxValue, - UShortAsSmallint = ushort.MaxValue, - UintAsBigint = uint.MaxValue, - UShortAsInt = ushort.MaxValue, - BoolAsBoolean = true, - DecimalAsMoney = 81.1m, - Decimal = 101.7m, - DecimalAsNumeric = 103.9m, - BigIntegerAsNumeric = 105.4m, - FloatAsReal = 84.4f, - DoubleAsDoublePrecision = 85.5, - DateTimeAsTimestamp = new DateTime(2015, 1, 2, 10, 11, 12), - DateTimeAsTimestamptz = new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc), - DateTimeAsDate = new DateTime(2015, 1, 2, 0, 0, 0), - TimeSpanAsTime = new TimeSpan(11, 15, 12), - DateTimeOffsetAsTimetz = new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)), - TimeSpanAsInterval = new TimeSpan(11, 15, 12), - DateOnlyAsDate = new DateOnly(2015, 1, 2), - TimeOnlyAsTime = new TimeOnly(11, 15, 12), - StringAsText = "Gumball Rules!", - StringAsVarchar = "Gumball Rules OK", - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // CharAsChar1 = 'f', - CharAsText = 'g', - CharAsVarchar = 'h', - 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"}""", - DictionaryAsHstore = new Dictionary { { "a", "b" } }, - ImmutableDictionaryAsHstore = ImmutableDictionary.Empty.Add("c", "d"), - NpgsqlRangeAsRange = new NpgsqlRange(4, true, 8, false), - IntArrayAsIntArray = [2, 3], - PhysicalAddressArrayAsMacaddrArray = - [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"), - SearchVector = NpgsqlTsVector.Parse("a b"), -#pragma warning restore CS0618 - RankingNormalization = NpgsqlTsRankingNormalization.DivideByLength, - Regconfig = 12724, - Mood = Mood.Sad - }); - - Assert.Equal(1, context.SaveChanges()); - } - - using (var context = CreateContext()) - { - var entity = context.Set().Single(e => e.Int == 999); - - long? param1 = 78L; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.LongAsBigint == param1)); - - short? param2 = 79; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.ShortAsSmallint == param2)); - - byte? param2a = 80; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.ByteAsSmallint == param2a)); - - uint? param3 = uint.MaxValue; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsInt == param3)); - - ulong? param4 = ulong.MaxValue; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UlongAsBigint == param4)); - - ushort? param5 = ushort.MaxValue; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UShortAsSmallint == param5)); - - uint? param6 = uint.MaxValue; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsBigint == param6)); - - ushort? param7 = ushort.MaxValue; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UShortAsInt == param7)); - - bool? param8 = true; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.BoolAsBoolean == param8)); - - // PostgreSQL doesn't support comparing money to decimal - //decimal? param9 = 81.1m; - //Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DecimalAsMoney == param9)); - - decimal? param10 = 101.7m; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Decimal == param10)); - - decimal? param11 = 103.9m; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DecimalAsNumeric == param11)); - - decimal? param12a = 105.4m; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.BigIntegerAsNumeric == param12a)); - - float? param12 = 84.4f; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.FloatAsReal == param12)); - - double? param13 = 85.5; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DoubleAsDoublePrecision == param13)); - - DateTime? param14 = new DateTime(2015, 1, 2, 10, 11, 12); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeAsTimestamp == param14)); - - DateTime? param15 = new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeAsTimestamptz == param15)); - - DateTime? param16 = new DateTime(2015, 1, 2, 0, 0, 0); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeAsDate == param16)); - - TimeSpan? param17 = new TimeSpan(11, 15, 12); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.TimeSpanAsTime == param17)); - - DateTimeOffset? param18 = new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateTimeOffsetAsTimetz == param18)); - - TimeSpan? param19 = new TimeSpan(11, 15, 12); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.TimeSpanAsInterval == param19)); - - DateOnly? param20 = new DateOnly(2015, 1, 2); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DateOnlyAsDate == param20)); - - TimeOnly? param21 = new TimeOnly(11, 15, 12); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.TimeOnlyAsTime == param21)); - - // ReSharper disable once ConvertToConstant.Local - var param22 = "Gumball Rules!"; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsText == param22)); - - // ReSharper disable once ConvertToConstant.Local - var param23 = "Gumball Rules OK"; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsVarchar == param23)); - - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // var param23a = 'f'; - // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.CharAsChar1 == param23a)); - - // ReSharper disable once ConvertToConstant.Local - var param23b = 'g'; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.CharAsText == param23b)); - - // ReSharper disable once ConvertToConstant.Local - var param23c = 'h'; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.CharAsVarchar == param23c)); - - var param24 = new byte[] { 86 }; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.BytesAsBytea == param24)); - - Guid? param25 = new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.GuidAsUuid == param25)); - - StringEnum16? param26 = StringEnum16.Value4; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.EnumAsText == param26)); - - StringEnumU16? param27 = StringEnumU16.Value4; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.EnumAsVarchar == param27)); - - var param28 = PhysicalAddress.Parse("08-00-2B-01-02-03"); - Assert.Same( - entity, context.Set().Single(e => e.Int == 999 && e.PhysicalAddressAsMacaddr.Equals(param28))); - - // PostgreSQL doesn't support equality comparison on point - // NpgsqlPoint? param29 = new NpgsqlPoint(5.2, 3.3); - // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Point == param29)); - - // ReSharper disable once ConvertToConstant.Local - 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) - // var param31 = @"{""a"": ""b""}"; - // Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.StringAsJson == param31)); - - var param32 = new Dictionary { { "a", "b" } }; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DictionaryAsHstore == param32)); - - var param33 = ImmutableDictionary.Empty.Add("c", "d"); - Assert.Same( - entity, context.Set().Single(e => e.Int == 999 && e.ImmutableDictionaryAsHstore == param33)); - - var param34 = new NpgsqlRange(4, true, 8, false); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.NpgsqlRangeAsRange == param34)); - - var param35 = new[] { 2, 3 }; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.IntArrayAsIntArray == param35)); - - var param36 = new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }; - Assert.Same( - entity, - context.Set().Single(e => e.Int == 999 && e.PhysicalAddressArrayAsMacaddrArray == param36)); - - // ReSharper disable once ConvertToConstant.Local - var param37 = (uint)int.MaxValue + 1; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsXid == param37)); - -#pragma warning disable CS0618 // Full-text search client-parsing is obsolete - var param38 = NpgsqlTsQuery.Parse("a & b"); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchQuery == param38)); - - var param39 = NpgsqlTsVector.Parse("a b"); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchVector == param39)); -#pragma warning restore CS0618 - - // ReSharper disable once ConvertToConstant.Local - var param40 = NpgsqlTsRankingNormalization.DivideByLength; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.RankingNormalization == param40)); - - // ReSharper disable once ConvertToConstant.Local - var param41 = 12724u; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Regconfig == param41)); - - // ReSharper disable once ConvertToConstant.Local - var param42 = Mood.Sad; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Mood == param42)); - } - } - - [Fact] - public virtual void Can_query_using_any_mapped_data_types_with_nulls() - { - using (var context = CreateContext()) - { - context.Set().Add( - new MappedNullableDataTypes { Int = 911, }); - - Assert.Equal(1, context.SaveChanges()); - } - - using (var context = CreateContext()) - { - var entity = context.Set().Single(e => e.Int == 911); - - long? param1 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.LongAsBigint == param1)); - - short? param2 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.ShortAsSmallint == param2)); - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && (long?)e.ShortAsSmallint == param2)); - - byte? param2a = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.ByteAsSmallint == param2a)); - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && (long?)e.ByteAsSmallint == param2a)); - - uint? param3 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsInt == param3)); - - ulong? param4 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UlongAsBigint == param4)); - - ushort? param5 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UShortAsSmallint == param5)); - - uint? param6 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsBigint == param6)); - - ushort? param7 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UShortAsInt == param7)); - - bool? param8 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.BoolAsBoolean == param8)); - - decimal? param9 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DecimalAsMoney == param9)); - - decimal? param10 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Decimal == param10)); - - decimal? param11 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DecimalAsNumeric == param11)); - - decimal? param111 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.BigIntegerAsNumeric == param111)); - - float? param12 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.FloatAsReal == param12)); - - double? param13 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DoubleAsDoublePrecision == param13)); - - DateTime? param14 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeAsTimestamp == param14)); - - DateTime? param15 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeAsTimestamptz == param15)); - - DateTime? param16 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeAsDate == param16)); - - TimeSpan? param17 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.TimeSpanAsTime == param17)); - - DateTimeOffset? param18 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateTimeOffsetAsTimetz == param18)); - - TimeSpan? param19 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.TimeSpanAsInterval == param19)); - - DateOnly? param20 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DateOnlyAsDate == param20)); - - TimeOnly? param21 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.TimeOnlyAsTime == param21)); - - string param22 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsText == param22)); - - string param23 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsVarchar == param23)); - - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // char? param23a = null; - // Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.CharAsChar1 == param23a)); - - char? param23b = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.CharAsText == param23b)); - - char? param23c = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.CharAsVarchar == param23c)); - - byte[] param24 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.BytesAsBytea == param24)); - - Guid? param25 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.GuidAsUuid == param25)); - - StringEnum16? param26 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.EnumAsText == param26)); - - StringEnumU16? param27 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.EnumAsVarchar == param27)); - - PhysicalAddress param28 = null; - // ReSharper disable once PossibleUnintendedReferenceComparison - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.PhysicalAddressAsMacaddr == param28)); - - // PostgreSQL does not support equality comparison on geometry types, see https://www.postgresql.org/docs/current/functions-geometry.html - //NpgsqlPoint? param29 = null; - //Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.NpgsqlPointAsPoint == param29)); - - string param30 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsJsonb == param30)); - - // PostgreSQL does not support equality comparison on json - //string param31 = null; - //Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.StringAsJson == param31)); - - Dictionary param32 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DictionaryAsHstore == param32)); - - ImmutableDictionary param33 = null; - Assert.Same( - entity, context.Set().Single(e => e.Int == 911 && e.ImmutableDictionaryAsHstore == param33)); - - NpgsqlRange? param34 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.NpgsqlRangeAsRange == param34)); - - int[] param35 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.IntArrayAsIntArray == param35)); - - PhysicalAddress[] param36 = null; - Assert.Same( - entity, - context.Set().Single(e => e.Int == 911 && e.PhysicalAddressArrayAsMacaddrArray == param36)); - - uint? param37 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsXid == param37)); - - NpgsqlTsQuery param38 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchQuery == param38)); - - NpgsqlTsVector param39 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchVector == param39)); - - NpgsqlTsRankingNormalization? param40 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.RankingNormalization == param40)); - - uint? param41 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Regconfig == param41)); - - Mood? param42 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Mood == param42)); - } - } - - [Fact] - public virtual void Can_insert_and_read_back_all_mapped_data_types() - { - var entity = CreateMappedDataTypes(77); - using var context = CreateContext(); - context.Set().Add(entity); - - Assert.Equal(1, context.SaveChanges()); - - var parameters = DumpParameters(); - Assert.Equal( - """ -@p0='77' -@p1='True' -@p2='80' -@p3='0x56' (Nullable = false) -@p4='g' (Nullable = false) -@p5='h' (Nullable = false) -@p6='2015-01-02T00:00:00.0000000' (DbType = Date) -@p7='2015-01-02T10:11:12.0000000' -@p8='2016-01-02T11:11:12.0000000Z' (DbType = DateTime) -@p9='0001-01-01T12:00:00.0000000+02:00' (DbType = Object) -@p10='101.7' -@p11='81.1' (DbType = Currency) -@p12='103.9' -@p13='System.Collections.Generic.Dictionary`2[System.String,System.String]' (Nullable = false) (DbType = Object) -@p14='85.5' -@p15='Value4' (Nullable = false) -@p16='Value4' (Nullable = false) -@p17='84.4' -@p18='a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' -@p19={ '2' -'3' } (Nullable = false) (DbType = Object) -@p20='78' -@p21='Sad' (DbType = Object) -@p22='(5.2,3.3)' (DbType = Object) -@p23='[4,8)' (DbType = Object) -@p24={ '08002B010203' -'08002B010204' } (Nullable = false) (DbType = Object) -@p25='08002B010203' (Nullable = false) (DbType = Object) -@p26='2' -@p27='12724' (DbType = Object) -@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) -@p33='Gumball Rules!' (Nullable = false) -@p34='Gumball Rules OK' (Nullable = false) -@p35='11:15:12' (DbType = Object) -@p36='11:15:12' -@p37='65535' -@p38='-1' -@p39='4294967295' -@p40='-1' -@p41='2147483648' (DbType = Object) -@p42='-1' -""", - parameters, - ignoreLineEndingDifferences: true); - } - - private string DumpParameters() - => Fixture.TestSqlLoggerFactory.Parameters.Single().Replace(", ", Environment.NewLine); - - // ReSharper disable once UnusedMember.Local - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - private static void AssertMappedDataTypes(MappedDataTypes entity, int id) - { - // ReSharper disable once UnusedVariable - var expected = CreateMappedDataTypes(id); - Assert.Equal(id, entity.Int); - Assert.Equal(78, entity.LongAsBigint); - Assert.Equal(79, entity.ShortAsSmallint); - Assert.Equal(uint.MaxValue, entity.UintAsInt); - Assert.Equal(ulong.MaxValue, entity.UlongAsBigint); - Assert.Equal(ushort.MaxValue, entity.UShortAsSmallint); - Assert.Equal(uint.MaxValue, entity.UintAsBigint); - Assert.Equal(ushort.MaxValue, entity.UShortAsInt); - - Assert.True(entity.BoolAsBoolean); - - Assert.Equal(81.1m, entity.DecimalAsMoney); - Assert.Equal(101.7m, entity.Decimal); - Assert.Equal(103.9m, entity.DecimalAsNumeric); - Assert.Equal(84.4f, entity.FloatAsReal); - Assert.Equal(85.5, entity.DoubleAsDoublePrecision); - - Assert.Equal(new DateTime(2015, 1, 2, 10, 11, 12), entity.DateTimeAsTimestamp); - Assert.Equal(new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc), entity.DateTimeAsTimestamptz); - Assert.Equal(new DateTime(2015, 1, 2, 0, 0, 0), entity.DateTimeAsDate); - Assert.Equal(new TimeSpan(11, 15, 12), entity.TimeSpanAsTime); - Assert.Equal(new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)), entity.DateTimeOffsetAsTimetz); - Assert.Equal(new TimeSpan(11, 15, 12), entity.TimeSpanAsInterval); - - Assert.Equal("Gumball Rules!", entity.StringAsText); - Assert.Equal("Gumball Rules OK", entity.StringAsVarchar); - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // Assert.Equal('f', entity.CharAsChar1); - Assert.Equal('g', entity.CharAsText); - Assert.Equal('h', entity.CharAsVarchar); - Assert.Equal([86], entity.BytesAsBytea); - - Assert.Equal(new Guid("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"), entity.GuidAsUuid); - - Assert.Equal(StringEnum16.Value4, entity.EnumAsText); - Assert.Equal(StringEnumU16.Value4, entity.EnumAsVarchar); - - 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(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( - [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); - -#pragma warning disable CS0618 // Full-text search client-parsing is obsolete - Assert.Equal(NpgsqlTsQuery.Parse("a & b").ToString(), entity.SearchQuery.ToString()); - Assert.Equal(NpgsqlTsVector.Parse("a b").ToString(), entity.SearchVector.ToString()); -#pragma warning restore CS0618 - Assert.Equal(NpgsqlTsRankingNormalization.DivideByLength, entity.RankingNormalization); - } - - private static MappedDataTypes CreateMappedDataTypes(int id) - => new() - { - Int = id, - LongAsBigint = 78L, - ShortAsSmallint = 79, - ByteAsSmallint = 80, - UintAsInt = uint.MaxValue, - UlongAsBigint = ulong.MaxValue, - UShortAsSmallint = ushort.MaxValue, - UintAsBigint = uint.MaxValue, - UShortAsInt = ushort.MaxValue, - BoolAsBoolean = true, - DecimalAsMoney = 81.1m, - Decimal = 101.7m, - DecimalAsNumeric = 103.9m, - FloatAsReal = 84.4f, - DoubleAsDoublePrecision = 85.5, - DateTimeAsTimestamp = new DateTime(2015, 1, 2, 10, 11, 12), - DateTimeAsTimestamptz = new DateTime(2016, 1, 2, 11, 11, 12, DateTimeKind.Utc), - DateTimeAsDate = new DateTime(2015, 1, 2, 0, 0, 0), - TimeSpanAsTime = new TimeSpan(11, 15, 12), - DateTimeOffsetAsTimetz = new DateTimeOffset(1, 1, 1, 12, 0, 0, TimeSpan.FromHours(2)), - TimeSpanAsInterval = new TimeSpan(11, 15, 12), - StringAsText = "Gumball Rules!", - StringAsVarchar = "Gumball Rules OK", - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // CharAsChar1 = 'f', - CharAsText = 'g', - CharAsVarchar = 'h', - 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"}""", - DictionaryAsHstore = new Dictionary { { "a", "b" } }, - NpgsqlRangeAsRange = new NpgsqlRange(4, true, 8, false), - IntArrayAsIntArray = [2, 3], - PhysicalAddressArrayAsMacaddrArray = - [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"), - SearchVector = NpgsqlTsVector.Parse("a b"), -#pragma warning restore CS0618 - RankingNormalization = NpgsqlTsRankingNormalization.DivideByLength, - Regconfig = 12724, - Mood = Mood.Sad - }; - - [Fact] - public virtual void Can_insert_and_read_back_all_mapped_data_types_set_to_null() - { - using (var context = CreateContext()) - { - context.Set().Add(new MappedNullableDataTypes { Int = 78 }); - - Assert.Equal(1, context.SaveChanges()); - } - - using (var context = CreateContext()) - { - AssertNullMappedNullableDataTypes(context.Set().Single(e => e.Int == 78), 78); - } - } - - // ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local - private static void AssertNullMappedNullableDataTypes(MappedNullableDataTypes entity, int id) - // ReSharper restore ParameterOnlyUsedForPreconditionCheck.Local - { - Assert.Equal(id, entity.Int); - Assert.Null(entity.LongAsBigint); - Assert.Null(entity.ShortAsSmallint); - Assert.Null(entity.ByteAsSmallint); - Assert.Null(entity.UintAsInt); - Assert.Null(entity.UlongAsBigint); - Assert.Null(entity.UShortAsSmallint); - Assert.Null(entity.UintAsBigint); - Assert.Null(entity.UShortAsInt); - - Assert.Null(entity.BoolAsBoolean); - - Assert.Null(entity.DecimalAsMoney); - Assert.Null(entity.Decimal); - Assert.Null(entity.DecimalAsNumeric); - Assert.Null(entity.FloatAsReal); - Assert.Null(entity.DoubleAsDoublePrecision); - - Assert.Null(entity.DateTimeAsTimestamp); - Assert.Null(entity.DateTimeAsTimestamptz); - Assert.Null(entity.DateTimeAsDate); - Assert.Null(entity.TimeSpanAsTime); - Assert.Null(entity.DateTimeOffsetAsTimetz); - Assert.Null(entity.TimeSpanAsInterval); - - Assert.Null(entity.DateOnlyAsDate); - Assert.Null(entity.TimeOnlyAsTime); - - Assert.Null(entity.StringAsText); - Assert.Null(entity.StringAsVarchar); - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // Assert.Null(entity.CharAsChar1); - Assert.Null(entity.CharAsText); - Assert.Null(entity.CharAsVarchar); - Assert.Null(entity.BytesAsBytea); - - Assert.Null(entity.GuidAsUuid); - - Assert.Null(entity.EnumAsText); - Assert.Null(entity.EnumAsVarchar); - - Assert.Null(entity.PhysicalAddressAsMacaddr); - Assert.Null(entity.NpgsqlPointAsPoint); - Assert.Null(entity.StringAsJsonb); - Assert.Null(entity.StringAsJson); - Assert.Null(entity.DictionaryAsHstore); - Assert.Null(entity.ImmutableDictionaryAsHstore); - Assert.Null(entity.NpgsqlRangeAsRange); - - Assert.Null(entity.IntArrayAsIntArray); - Assert.Null(entity.PhysicalAddressArrayAsMacaddrArray); - - Assert.Null(entity.UintAsXid); - - Assert.Null(entity.SearchQuery); - Assert.Null(entity.SearchVector); - Assert.Null(entity.RankingNormalization); - - Assert.Null(entity.Mood); - } - - 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, await context.SaveChangesAsync()); - } - - using (var context = CreateContext()) - { - var entity = (await context.Set().Where(e => e.Id == 711).ToListAsync()).Single(); - - short? param1 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt16 == param1).ToListAsync()) - .Single()); - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && (long?)e.TestNullableInt16 == param1) - .ToListAsync()) - .Single()); - - int? param2 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt32 == param2).ToListAsync()) - .Single()); - - long? param3 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableInt64 == param3).ToListAsync()) - .Single()); - - double? param4 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableDouble == param4).ToListAsync()) - .Single()); - - decimal? param5 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableDecimal == param5).ToListAsync()) - .Single()); - - DateTime? param6 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableDateTime == param6).ToListAsync()) - .Single()); - - // We don't support DateTimeOffset - - TimeSpan? param8 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableTimeSpan == param8).ToListAsync()) - .Single()); - - float? param9 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableSingle == param9).ToListAsync()) - .Single()); - - bool? param10 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableBoolean == param10).ToListAsync()) - .Single()); - - // We don't support byte - - Enum64? param12 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.Enum64 == param12).ToListAsync()).Single()); - - Enum32? param13 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.Enum32 == param13).ToListAsync()).Single()); - - Enum16? param14 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.Enum16 == param14).ToListAsync()).Single()); - - Enum8? param15 = null; - Assert.Same( - 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) - { - ushort? param16 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt16 == param16) - .ToListAsync()) - .Single()); - } - - if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt32)) is not null) - { - uint? param17 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt32 == param17) - .ToListAsync()) - .Single()); - } - - if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableUnsignedInt64)) is not null) - { - ulong? param18 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableUnsignedInt64 == param18) - .ToListAsync()) - .Single()); - } - - if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableCharacter)) is not null) - { - char? param19 = null; - Assert.Same( - entity, - (await context.Set().Where(e => e.Id == 711 && e.TestNullableCharacter == param19) - .ToListAsync()) - .Single()); - } - - if (entityType.FindProperty(nameof(BuiltInNullableDataTypes.TestNullableSignedByte)) is not null) - { - sbyte? param20 = null; - Assert.Same( - entity, - (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, - (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, - (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, - (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, - (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 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 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 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 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 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 Task Can_query_using_any_data_type_shadow() - => Task.CompletedTask; - - [ConditionalFact] - public void Sum_Conversions() - { - using var context = CreateContext(); - - // PostgreSQL SUM() returns numeric for bigint input, bigint for int/smallint ints. - // Make sure the proper conversion is done - _ = context.Set().Sum(m => m.LongAsBigint); - _ = context.Set().Sum(m => m.Int); - _ = context.Set().Sum(m => m.ShortAsSmallint); - - AssertSql( - """ -SELECT COALESCE(sum(m."LongAsBigint"), 0.0)::bigint -FROM "MappedDataTypes" AS m -""", - // - """ -SELECT COALESCE(sum(m."Int"), 0)::int -FROM "MappedDataTypes" AS m -""", - // - """ -SELECT COALESCE(sum(m."ShortAsSmallint"::int), 0)::int -FROM "MappedDataTypes" AS m -"""); - } - - [ConditionalFact] - public void Money_compare_constant() - { - using var context = CreateContext(); - - _ = context.Set().Where(m => m.DecimalAsMoney > 3).ToList(); - } - - [ConditionalFact] - public void Money_compare_parameter() - { - using var context = CreateContext(); - - var money = 3m; - _ = context.Set().Where(m => m.DecimalAsMoney > money).ToList(); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class BuiltInDataTypesNpgsqlFixture : BuiltInDataTypesFixtureBase - { - public override bool StrictEquality - => false; - - public override bool SupportsAnsi - => false; - - public override bool SupportsUnicodeToAnsiConversion - => false; - - public override bool SupportsLargeStringComparisons - => true; - - public override bool SupportsDecimalComparisons - => true; - - 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 - => new NpgsqlTestStoreFactory(useConnectionString: true); - - protected override bool ShouldLogCategory(string logCategory) - => logCategory == DbLoggerCategory.Query.Name; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - 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); - - MakeRequired(modelBuilder); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(b => b.TestDateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.TestNullableDateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(nameof(BuiltInNullableDataTypes.TestNullableDateTime)) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - - // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data - 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); - - 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); - - modelBuilder.Entity().Metadata.GetSeedData() - .Single()[nameof(ObjectBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(new DateTime(), TimeSpan.Zero); - - modelBuilder.Entity().Metadata.GetSeedData() - .Single()[nameof(NullableBackedDataTypes.DateTimeOffset)] - = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.Zero); - - modelBuilder.Entity().Metadata.GetSeedData() - .Single()[nameof(NonNullableBackedDataTypes.DateTimeOffset)] = new DateTimeOffset(new DateTime(), TimeSpan.Zero); - - modelBuilder.Entity( - b => - { - b.Ignore(dt => dt.TestUnsignedInt16); - b.Ignore(dt => dt.TestUnsignedInt32); - b.Ignore(dt => dt.TestUnsignedInt64); - b.Ignore(dt => dt.TestCharacter); - b.Ignore(dt => dt.TestSignedByte); - b.Ignore(dt => dt.TestDateTimeOffset); - b.Ignore(dt => dt.TestByte); - //b.Ignore(dt => dt.EnumU16); - //b.Ignore(dt => dt.EnumU32); - //b.Ignore(dt => dt.EnumU64); - //b.Ignore(dt => dt.EnumS8); - }); - - modelBuilder.Entity( - b => - { - b.Ignore(dt => dt.TestNullableUnsignedInt16); - b.Ignore(dt => dt.TestNullableUnsignedInt32); - b.Ignore(dt => dt.TestNullableUnsignedInt64); - b.Ignore(dt => dt.TestNullableCharacter); - b.Ignore(dt => dt.TestNullableSignedByte); - b.Ignore(dt => dt.TestNullableDateTimeOffset); - b.Ignore(dt => dt.TestNullableByte); - //b.Ignore(dt => dt.EnumU16); - //b.Ignore(dt => dt.EnumU32); - //b.Ignore(dt => dt.EnumU64); - //b.Ignore(dt => dt.EnumS8); - }); - - modelBuilder.Entity( - b => - { - b.HasKey(e => e.Int); - b.Property(e => e.Int).ValueGeneratedNever(); - }); - - modelBuilder.Entity( - b => - { - b.HasKey(e => e.Int); - b.Property(e => e.Int).ValueGeneratedNever(); - }); - - modelBuilder.Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - - modelBuilder.Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - - modelBuilder.Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - - // Full text - modelBuilder.Entity().Property(x => x.SearchQuery).HasColumnType("tsquery"); - modelBuilder.Entity().Property(x => x.SearchVector).HasColumnType("tsvector"); - modelBuilder.Entity().Property(x => x.RankingNormalization).HasColumnType("integer"); - modelBuilder.Entity().Property(x => x.Regconfig).HasColumnType("regconfig"); - modelBuilder.Entity().Property(x => x.SearchQuery).HasColumnType("tsquery"); - modelBuilder.Entity().Property(x => x.SearchVector).HasColumnType("tsvector"); - modelBuilder.Entity().Property(x => x.RankingNormalization).HasColumnType("integer"); - modelBuilder.Entity().Property(x => x.Regconfig).HasColumnType("regconfig"); - } - - public override bool SupportsBinaryKeys - => true; - - public override DateTime DefaultDateTime - => new(); - } - - protected enum StringEnum16 : short - { - // ReSharper disable once UnusedMember.Global - Value1 = 1, - - // ReSharper disable once UnusedMember.Global - Value2 = 2, - Value4 = 4 - } - - protected enum StringEnumU16 : ushort - { - // ReSharper disable once UnusedMember.Global - Value1 = 1, - - // ReSharper disable once UnusedMember.Global - Value2 = 2, - Value4 = 4 - } - - // ReSharper disable once MemberCanBePrivate.Global - protected class MappedDataTypes - { - [Column(TypeName = "int")] - public int Int { get; set; } - - [Column(TypeName = "bigint")] - public long LongAsBigint { get; set; } - - [Column(TypeName = "smallint")] - public short ShortAsSmallint { get; set; } - - [Column(TypeName = "smallint")] - public byte ByteAsSmallint { get; set; } - - [Column(TypeName = "int")] - public uint UintAsInt { get; set; } - - [Column(TypeName = "bigint")] - public uint UintAsBigint { get; set; } - - [Column(TypeName = "bigint")] - public ulong UlongAsBigint { get; set; } - - [Column(TypeName = "smallint")] - public ushort UShortAsSmallint { get; set; } - - [Column(TypeName = "int")] - public ushort UShortAsInt { get; set; } - - //[Column(TypeName = "tinyint")] - //public sbyte SByteAsTinyint { get; set; } - - [Column(TypeName = "boolean")] - public bool BoolAsBoolean { get; set; } - - [Column(TypeName = "numeric")] - public decimal Decimal { get; set; } // decimal is just an alias for numeric - - [Column(TypeName = "numeric")] - public decimal DecimalAsNumeric { get; set; } - - [Column(TypeName = "money")] - public decimal DecimalAsMoney { get; set; } - - [Column(TypeName = "double precision")] - public double DoubleAsDoublePrecision { get; set; } - - [Column(TypeName = "real")] - public float FloatAsReal { get; set; } - - [Column(TypeName = "timestamp")] - public DateTime DateTimeAsTimestamp { get; set; } - - [Column(TypeName = "timestamptz")] - public DateTime DateTimeAsTimestamptz { get; set; } - - [Column(TypeName = "date")] - public DateTime DateTimeAsDate { get; set; } - - [Column(TypeName = "time")] - public TimeSpan TimeSpanAsTime { get; set; } - - [Column(TypeName = "timetz")] - public DateTimeOffset DateTimeOffsetAsTimetz { get; set; } - - [Column(TypeName = "interval")] - public TimeSpan TimeSpanAsInterval { get; set; } - - [Column(TypeName = "text")] - public string StringAsText { get; set; } - - [Column(TypeName = "varchar")] - public string StringAsVarchar { get; set; } - - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // [Column(TypeName = "char(1)")] - // public char? CharAsChar1 { get; set; } - - [Column(TypeName = "text")] - public char? CharAsText { get; set; } - - [Column(TypeName = "varchar")] - public char? CharAsVarchar { get; set; } - - [Column(TypeName = "bytea")] - public byte[] BytesAsBytea { get; set; } - - [Column(TypeName = "uuid")] - public Guid GuidAsUuid { get; set; } - - [Column(TypeName = "text")] - public StringEnum16 EnumAsText { get; set; } - - [Column(TypeName = "varchar")] - public StringEnumU16 EnumAsVarchar { get; set; } - - // PostgreSQL-specific types from here - - [Column(TypeName = "macaddr")] - public PhysicalAddress PhysicalAddressAsMacaddr { get; set; } - - [Column(TypeName = "point")] - public NpgsqlPoint NpgsqlPointAsPoint { get; set; } - - [Column(TypeName = "jsonb")] - public string StringAsJsonb { get; set; } - - [Column(TypeName = "json")] - public string StringAsJson { get; set; } - - [Column(TypeName = "hstore")] - public Dictionary DictionaryAsHstore { get; set; } - - [Column(TypeName = "int4range")] - public NpgsqlRange NpgsqlRangeAsRange { get; set; } - - [Column(TypeName = "int[]")] - public int[] IntArrayAsIntArray { get; set; } - - [Column(TypeName = "macaddr[]")] - public PhysicalAddress[] PhysicalAddressArrayAsMacaddrArray { get; set; } - - [Column(TypeName = "xid")] - public uint UintAsXid { get; set; } - - public NpgsqlTsQuery SearchQuery { get; set; } - public NpgsqlTsVector SearchVector { get; set; } - public NpgsqlTsRankingNormalization RankingNormalization { get; set; } - public uint Regconfig { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Global - [Column(TypeName = "mood")] - public Mood Mood { get; set; } - } - - // ReSharper disable once MemberCanBePrivate.Global - public class MappedSizedDataTypes - { - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public int Id { get; set; } - /* - public string Char { get; set; } - public string Character { get; set; } - public string Varchar { get; set; } - public string Char_varying { get; set; } - public string Character_varying { get; set; } - public string Nchar { get; set; } - public string National_character { get; set; } - public string Nvarchar { get; set; } - public string National_char_varying { get; set; } - public string National_character_varying { get; set; } - public byte[] Binary { get; set; } - public byte[] Varbinary { get; set; } - public byte[] Binary_varying { get; set; } - */ - } - - // ReSharper disable once MemberCanBePrivate.Global - public class MappedScaledDataTypes - { - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public int Id { get; set; } - /* - public float Float { get; set; } - public float Double_precision { get; set; } - public DateTimeOffset Datetimeoffset { get; set; } - public DateTime Datetime2 { get; set; } - public decimal Decimal { get; set; } - public decimal Dec { get; set; } - public decimal Numeric { get; set; } - */ - } - - // ReSharper disable once MemberCanBePrivate.Global - public class MappedPrecisionAndScaledDataTypes - { - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public int Id { get; set; } - /* - public decimal Decimal { get; set; } - public decimal Dec { get; set; } - public decimal Numeric { get; set; } - */ - } - - // ReSharper disable once MemberCanBePrivate.Global - protected class MappedNullableDataTypes - { - [Column(TypeName = "int")] - public int? Int { get; set; } - - [Column(TypeName = "bigint")] - public long? LongAsBigint { get; set; } - - [Column(TypeName = "smallint")] - public short? ShortAsSmallint { get; set; } - - [Column(TypeName = "smallint")] - public byte? ByteAsSmallint { get; set; } - - [Column(TypeName = "int")] - public uint? UintAsInt { get; set; } - - [Column(TypeName = "bigint")] - public uint? UintAsBigint { get; set; } - - [Column(TypeName = "bigint")] - public ulong? UlongAsBigint { get; set; } - - [Column(TypeName = "smallint")] - public ushort? UShortAsSmallint { get; set; } - - [Column(TypeName = "int")] - public ushort? UShortAsInt { get; set; } - - //[Column(TypeName = "tinyint")] - //public sbyte? SByteAsTinyint { get; set; } - - [Column(TypeName = "boolean")] - public bool? BoolAsBoolean { get; set; } - - [Column(TypeName = "numeric")] - public decimal? Decimal { get; set; } // decimal is just an alias for numeric - - [Column(TypeName = "numeric")] - public decimal? DecimalAsNumeric { get; set; } - - [Column(TypeName = "money")] - public decimal? DecimalAsMoney { get; set; } - - [Column(TypeName = "numeric")] - public decimal? BigIntegerAsNumeric { get; set; } - - [Column(TypeName = "double precision")] - public double? DoubleAsDoublePrecision { get; set; } - - [Column(TypeName = "real")] - public float? FloatAsReal { get; set; } - - [Column(TypeName = "timestamp")] - public DateTime? DateTimeAsTimestamp { get; set; } - - [Column(TypeName = "timestamptz")] - public DateTime? DateTimeAsTimestamptz { get; set; } - - [Column(TypeName = "date")] - public DateTime? DateTimeAsDate { get; set; } - - [Column(TypeName = "time")] - public TimeSpan? TimeSpanAsTime { get; set; } - - [Column(TypeName = "date")] - public DateOnly? DateOnlyAsDate { get; set; } - - [Column(TypeName = "time")] - public TimeOnly? TimeOnlyAsTime { get; set; } - - [Column(TypeName = "timetz")] - public DateTimeOffset? DateTimeOffsetAsTimetz { get; set; } - - [Column(TypeName = "interval")] - public TimeSpan? TimeSpanAsInterval { get; set; } - - [Column(TypeName = "text")] - public string StringAsText { get; set; } - - [Column(TypeName = "varchar")] - public string StringAsVarchar { get; set; } - - // TODO: enable here (and above) after https://github.com/aspnet/EntityFrameworkCore/issues/14159 is fixed - // [Column(TypeName = "char(1)")] - // public char? CharAsChar1 { get; set; } - - [Column(TypeName = "text")] - public char? CharAsText { get; set; } - - [Column(TypeName = "varchar")] - public char? CharAsVarchar { get; set; } - - [Column(TypeName = "bytea")] - public byte[] BytesAsBytea { get; set; } - - [Column(TypeName = "uuid")] - public Guid? GuidAsUuid { get; set; } - - [Column(TypeName = "text")] - public StringEnum16? EnumAsText { get; set; } - - [Column(TypeName = "varchar")] - public StringEnumU16? EnumAsVarchar { get; set; } - - // PostgreSQL-specific types from here - - [Column(TypeName = "macaddr")] - public PhysicalAddress PhysicalAddressAsMacaddr { get; set; } - - [Column(TypeName = "point")] - public NpgsqlPoint? NpgsqlPointAsPoint { get; set; } - - [Column(TypeName = "jsonb")] - public string StringAsJsonb { get; set; } - - [Column(TypeName = "json")] - public string StringAsJson { get; set; } - - [Column(TypeName = "hstore")] - public Dictionary DictionaryAsHstore { get; set; } - - [Column(TypeName = "hstore")] - public ImmutableDictionary ImmutableDictionaryAsHstore { get; set; } - - [Column(TypeName = "int4range")] - public NpgsqlRange? NpgsqlRangeAsRange { get; set; } - - [Column(TypeName = "int[]")] - public int[] IntArrayAsIntArray { get; set; } - - [Column(TypeName = "macaddr[]")] - public PhysicalAddress[] PhysicalAddressArrayAsMacaddrArray { get; set; } - - [Column(TypeName = "xid")] - public uint? UintAsXid { get; set; } - - public NpgsqlTsQuery SearchQuery { get; set; } - public NpgsqlTsVector SearchVector { get; set; } - public NpgsqlTsRankingNormalization? RankingNormalization { get; set; } - public uint? Regconfig { get; set; } - - [Column(TypeName = "mood")] - public Mood? Mood { get; set; } - } -} - -// ReSharper disable once UnusedMember.Global -public enum Mood { Happy, Sad } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs deleted file mode 100644 index 693f4fcaca..0000000000 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.BulkUpdates; - -public class NorthwindBulkUpdatesNpgsqlFixture : NorthwindBulkUpdatesRelationalFixture - where TModelCustomizer : ITestModelCustomizer, new() -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlNorthwindTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity() - .Property(p => p.UnitPrice) - .HasColumnType("money"); - } - - protected override Type ContextType - => typeof(NorthwindNpgsqlContext); -} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs deleted file mode 100644 index 9aaf4e69d3..0000000000 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.BulkUpdates; - -public class TPCFiltersInheritanceBulkUpdatesNpgsqlFixture : TPCInheritanceBulkUpdatesNpgsqlFixture -{ - public override bool EnableFilters - => true; -} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs deleted file mode 100644 index 1d2b2b8f10..0000000000 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.BulkUpdates; - -public class TPCInheritanceBulkUpdatesNpgsqlFixture : TPCInheritanceBulkUpdatesFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override bool UseGeneratedKeys - => false; -} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlFixture.cs deleted file mode 100644 index 281e76cfe5..0000000000 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPHFiltersInheritanceBulkUpdatesNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.BulkUpdates; - -public class TPHFiltersInheritanceBulkUpdatesNpgsqlFixture : TPHInheritanceBulkUpdatesNpgsqlFixture -{ - public override bool EnableFilters - => true; -} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs deleted file mode 100644 index 4069a3542d..0000000000 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.BulkUpdates; - -public class TPTFiltersInheritanceBulkUpdatesNpgsqlFixture : TPTInheritanceBulkUpdatesNpgsqlFixture -{ - public override bool EnableFilters - => true; -} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs deleted file mode 100644 index 67e773802c..0000000000 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.BulkUpdates; - -public class TPTInheritanceBulkUpdatesNpgsqlFixture : TPTInheritanceBulkUpdatesFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs deleted file mode 100644 index 225e1fc96b..0000000000 --- a/test/EFCore.PG.FunctionalTests/CommandInterceptionNpgsqlTest.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore; - -public abstract class CommandInterceptionNpgsqlTestBase(CommandInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) - : CommandInterceptionTestBase(fixture) -{ - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase - { - protected override string StoreName - => "CommandInterception"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override IServiceCollection InjectInterceptors( - IServiceCollection serviceCollection, - IEnumerable injectedInterceptors) - => base.InjectInterceptors(serviceCollection.AddEntityFrameworkNpgsql(), injectedInterceptors); - } - - public class CommandInterceptionNpgsqlTest(CommandInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) - : CommandInterceptionNpgsqlTestBase(fixture), IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override bool ShouldSubscribeToDiagnosticListener - => false; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder(base.AddOptions(builder)) - .ExecutionStrategy(d => new NpgsqlExecutionStrategy(d)); - return builder; - } - } - } - - public class CommandInterceptionWithDiagnosticsNpgsqlTest( - CommandInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) - : CommandInterceptionNpgsqlTestBase(fixture), IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override bool ShouldSubscribeToDiagnosticListener - => true; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder(base.AddOptions(builder)) - .ExecutionStrategy(d => new NpgsqlExecutionStrategy(d)); - return builder; - } - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/ComplexTypesTrackingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ComplexTypesTrackingNpgsqlTest.cs deleted file mode 100644 index 02e81489b5..0000000000 --- a/test/EFCore.PG.FunctionalTests/ComplexTypesTrackingNpgsqlTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class ComplexTypesTrackingNpgsqlTest(ComplexTypesTrackingNpgsqlTest.NpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexTypesTrackingRelationalTestBase(fixture, testOutputHelper) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - // 'timestamp with time zone' literal cannot be generated for Unspecified DateTime: a UTC DateTime is required - public override Task Can_track_entity_with_complex_property_bag_collections(EntityState state, bool async) - => Task.CompletedTask; - - public class NpgsqlFixture : RelationalFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs b/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs deleted file mode 100644 index 9b9081f7b2..0000000000 --- a/test/EFCore.PG.FunctionalTests/CompositeKeyEndToEndTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class CompositeKeyEndToEndNpgsqlTest(CompositeKeyEndToEndNpgsqlTest.CompositeKeyEndToEndNpgsqlFixture fixture) - : CompositeKeyEndToEndTestBase(fixture) -{ - public class CompositeKeyEndToEndNpgsqlFixture : CompositeKeyEndToEndFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs deleted file mode 100644 index 615c367f9b..0000000000 --- a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorDisabledNpgsqlTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class ConcurrencyDetectorDisabledNpgsqlTest : ConcurrencyDetectorDisabledRelationalTestBase< - ConcurrencyDetectorDisabledNpgsqlTest.ConcurrencyDetectorNpgsqlFixture> -{ - public ConcurrencyDetectorDisabledNpgsqlTest(ConcurrencyDetectorNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - 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()); - - protected override async Task ConcurrencyDetectorTest(Func> test) - { - await base.ConcurrencyDetectorTest(test); - - Assert.NotEmpty(Fixture.TestSqlLoggerFactory.SqlStatements); - } - - public class ConcurrencyDetectorNpgsqlFixture : ConcurrencyDetectorFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => builder.EnableThreadSafetyChecks(enableChecks: false); - } -} diff --git a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs deleted file mode 100644 index d9dd48f46b..0000000000 --- a/test/EFCore.PG.FunctionalTests/ConcurrencyDetectorEnabledNpgsqlTest.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class ConcurrencyDetectorEnabledNpgsqlTest : ConcurrencyDetectorEnabledRelationalTestBase< - ConcurrencyDetectorEnabledNpgsqlTest.ConcurrencyDetectorNpgsqlFixture> -{ - public ConcurrencyDetectorEnabledNpgsqlTest(ConcurrencyDetectorNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - 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()); - - protected override async Task ConcurrencyDetectorTest(Func> test) - { - await base.ConcurrencyDetectorTest(test); - - Assert.Empty(Fixture.TestSqlLoggerFactory.SqlStatements); - } - - public class ConcurrencyDetectorNpgsqlFixture : ConcurrencyDetectorFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - } -} diff --git a/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs deleted file mode 100644 index 2afacd122d..0000000000 --- a/test/EFCore.PG.FunctionalTests/ConferencePlannerNpgsqlTest.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore.TestModels.ConferencePlanner; - -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -public class ConferencePlannerNpgsqlTest(ConferencePlannerNpgsqlTest.ConferencePlannerNpgsqlFixture fixture) - : ConferencePlannerTestBase(fixture) -{ - // Overridden to use UTC DateTimeOffsets - public override async Task SessionsController_Post() - => await ExecuteWithStrategyInTransactionAsync( - async context => - { - var track = context.Tracks.AsNoTracking().OrderBy(e => e.Id).First(); - - var controller = new SessionsController(context); - - var result = await controller.Post( - new Session - { - Abstract = "Pandas eat bamboo all dat.", - Title = "Pandas!", - // Npgsql customizations - StartTime = DateTimeOffset.UtcNow, - EndTime = DateTimeOffset.UtcNow.AddHours(1), - TrackId = track.Id - }); - - var newSession = context.Sessions.AsNoTracking().Single(e => e.Title == "Pandas!"); - - Assert.Equal(newSession.Id, result.Id); - Assert.Null(result.Speakers); - Assert.NotNull(result.Track); - Assert.Equal(track.Id, result.Track.Id); - }); - - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public class ConferencePlannerNpgsqlFixture : ConferencePlannerFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - // 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 async Task SeedAsync(ApplicationDbContext context) - { - var attendees1 = new List - { - new() - { - EmailAddress = "sonicrainboom@sample.com", - FirstName = "Rainbow", - LastName = "Dash", - UserName = "RainbowDash" - }, - new() - { - EmailAddress = "solovely@sample.com", - FirstName = "Flutter", - LastName = "Shy", - UserName = "Fluttershy" - } - }; - - var attendees2 = new List - { - new() - { - EmailAddress = "applesforever@sample.com", - FirstName = "Apple", - LastName = "Jack", - UserName = "Applejack" - }, - new() - { - EmailAddress = "precious@sample.com", - FirstName = "Rarity", - LastName = "", - UserName = "Rarity" - } - }; - - var attendees3 = new List - { - new() - { - EmailAddress = "princess@sample.com", - FirstName = "Twilight", - LastName = "Sparkle", - UserName = "Princess" - }, - new() - { - EmailAddress = "pinkie@sample.com", - FirstName = "Pinkie", - LastName = "Pie", - UserName = "Pinks" - } - }; - - using var document = JsonDocument.Parse(ConferenceData); - - var tracks = new Dictionary(); - var speakers = new Dictionary(); - - var root = document.RootElement; - foreach (var dayJson in root.EnumerateArray()) - { - foreach (var roomJson in dayJson.GetProperty("rooms").EnumerateArray()) - { - var roomId = roomJson.GetProperty("id").GetInt32(); - if (!tracks.TryGetValue(roomId, out var track)) - { - track = new Track { Name = roomJson.GetProperty("name").GetString(), Sessions = new List() }; - - tracks[roomId] = track; - } - - foreach (var sessionJson in roomJson.GetProperty("sessions").EnumerateArray()) - { - var sessionSpeakers = new List(); - foreach (var speakerJson in sessionJson.GetProperty("speakers").EnumerateArray()) - { - var speakerId = speakerJson.GetProperty("id").GetGuid(); - if (!speakers.TryGetValue(speakerId, out var speaker)) - { - speaker = new Speaker { Name = speakerJson.GetProperty("name").GetString() }; - - speakers[speakerId] = speaker; - } - - sessionSpeakers.Add(speaker); - } - - var session = new Session - { - Title = sessionJson.GetProperty("title").GetString(), - Abstract = sessionJson.GetProperty("description").GetString(), - // Npgsql customizations - StartTime = sessionJson.GetProperty("startsAt").GetDateTime().ToUniversalTime(), - EndTime = sessionJson.GetProperty("endsAt").GetDateTime().ToUniversalTime() - }; - - session.SessionSpeakers = sessionSpeakers.Select( - s => new SessionSpeaker { Session = session, Speaker = s }).ToList(); - - var trackName = track.Name; - var attendees = trackName.Contains("1") ? attendees1 - : trackName.Contains("2") ? attendees2 - : trackName.Contains("3") ? attendees3 - : attendees1.Concat(attendees2).Concat(attendees3).ToList(); - - session.SessionAttendees = attendees.Select( - a => new SessionAttendee { Session = session, Attendee = a }).ToList(); - - track.Sessions.Add(session); - } - } - } - - context.AddRange(tracks.Values); - await context.SaveChangesAsync(); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs deleted file mode 100644 index ad7b852f13..0000000000 --- a/test/EFCore.PG.FunctionalTests/ConnectionInterceptionNpgsqlTest.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Data; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.EntityFrameworkCore; - -public abstract class ConnectionInterceptionNpgsqlTestBase(ConnectionInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) - : ConnectionInterceptionTestBase(fixture) -{ - [ConditionalTheory(Skip = "#2368")] - public override Task Intercept_connection_creation_passively(bool async) - => base.Intercept_connection_creation_passively(async); - - [ConditionalTheory(Skip = "#2368")] - public override Task Intercept_connection_creation_with_multiple_interceptors(bool async) - => base.Intercept_connection_creation_with_multiple_interceptors(async); - - [ConditionalTheory(Skip = "#2368")] - public override Task Intercept_connection_to_override_connection_after_creation(bool async) - => base.Intercept_connection_to_override_connection_after_creation(async); - - [ConditionalTheory(Skip = "#2368")] - public override Task Intercept_connection_to_override_creation(bool async) - => base.Intercept_connection_to_override_creation(async); - - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase - { - protected override string StoreName - => "ConnectionInterception"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override IServiceCollection InjectInterceptors( - IServiceCollection serviceCollection, - IEnumerable injectedInterceptors) - => base.InjectInterceptors(serviceCollection.AddEntityFrameworkNpgsql(), injectedInterceptors); - } - - protected override DbContextOptionsBuilder ConfigureProvider(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(); - - protected override BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder) - => new(optionsBuilder.UseNpgsql(new FakeDbConnection()).Options); - - public class FakeDbConnection : DbConnection - { - [AllowNull] - public override string ConnectionString { get; set; } - - public override string Database - => "Database"; - - public override string DataSource - => "DataSource"; - - public override string ServerVersion - => throw new NotImplementedException(); - - public override ConnectionState State - => ConnectionState.Closed; - - public override void ChangeDatabase(string databaseName) - => throw new NotImplementedException(); - - public override void Close() - => throw new NotImplementedException(); - - public override void Open() - => throw new NotImplementedException(); - - protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) - => throw new NotImplementedException(); - - protected override DbCommand CreateDbCommand() - => throw new NotImplementedException(); - } - - public class ConnectionInterceptionNpgsqlTest(ConnectionInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) - : ConnectionInterceptionNpgsqlTestBase(fixture), IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override bool ShouldSubscribeToDiagnosticListener - => false; - } - } - - public class ConnectionInterceptionWithDiagnosticsNpgsqlTest( - ConnectionInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) - : ConnectionInterceptionNpgsqlTestBase(fixture), - IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override bool ShouldSubscribeToDiagnosticListener - => true; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs b/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs deleted file mode 100644 index c16a173f0f..0000000000 --- a/test/EFCore.PG.FunctionalTests/ConnectionSpecificationTest.cs +++ /dev/null @@ -1,284 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable StringLiteralTypo -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -public class ConnectionSpecificationTest -{ - [Fact] - public async Task Can_specify_connection_string_in_OnConfiguring() - { - var serviceProvider = new ServiceCollection() - .AddDbContext() - .BuildServiceProvider(); - - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = serviceProvider.GetRequiredService(); - - Assert.True(await context.Customers.AnyAsync()); - } - - [Fact] - public async Task Can_specify_connection_string_in_OnConfiguring_with_default_service_provider() - { - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = new StringInOnConfiguringContext(); - - Assert.True(await context.Customers.AnyAsync()); - } - - private class StringInOnConfiguringContext : NorthwindContextBase - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(NpgsqlTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); - } - - [Fact] - public async Task Can_specify_connection_in_OnConfiguring() - { - var serviceProvider = new ServiceCollection() - .AddScoped(_ => new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)) - .AddDbContext().BuildServiceProvider(); - - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = serviceProvider.GetRequiredService(); - - Assert.True(await context.Customers.AnyAsync()); - } - - [Fact] - public async Task Can_specify_connection_in_OnConfiguring_with_default_service_provider() - { - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = new ConnectionInOnConfiguringContext(new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)); - - Assert.True(await context.Customers.AnyAsync()); - } - - private class ConnectionInOnConfiguringContext(NpgsqlConnection connection) : NorthwindContextBase - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(connection, b => b.ApplyConfiguration()); - - public override void Dispose() - { - connection.Dispose(); - base.Dispose(); - } - } - - // ReSharper disable once UnusedMember.Local - private class StringInConfigContext : NorthwindContextBase - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql("Database=Crunchie", b => b.ApplyConfiguration()); - } - - [Fact] - public void Throws_if_no_connection_found_in_config_without_UseNpgsql() - { - var serviceProvider = new ServiceCollection() - .AddDbContext().BuildServiceProvider(); - - using var context = serviceProvider.GetRequiredService(); - - Assert.Equal( - CoreStrings.NoProviderConfigured, - Assert.Throws(() => context.Customers.Any()).Message); - } - - [Fact] - public void Throws_if_no_config_without_UseNpgsql() - { - var serviceProvider = new ServiceCollection() - .AddDbContext().BuildServiceProvider(); - using var context = serviceProvider.GetRequiredService(); - - Assert.Equal( - CoreStrings.NoProviderConfigured, - Assert.Throws(() => context.Customers.Any()).Message); - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class NoUseNpgsqlContext : NorthwindContextBase; - - [Fact] - public async Task Can_depend_on_DbContextOptions() - { - var serviceProvider = new ServiceCollection() - .AddScoped(_ => new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)) - .AddDbContext() - .BuildServiceProvider(); - - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = serviceProvider.GetRequiredService(); - - Assert.True(await context.Customers.AnyAsync()); - } - - [Fact] - public async Task Can_depend_on_DbContextOptions_with_default_service_provider() - { - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = new OptionsContext( - new DbContextOptions(), - new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString)); - - Assert.True(await context.Customers.AnyAsync()); - } - - private class OptionsContext(DbContextOptions options, NpgsqlConnection connection) - : NorthwindContextBase(options) - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - Assert.Same(options, optionsBuilder.Options); - - optionsBuilder.UseNpgsql(connection, b => b.ApplyConfiguration()); - - Assert.NotSame(options, optionsBuilder.Options); - } - - public override void Dispose() - { - connection.Dispose(); - base.Dispose(); - } - } - - [Fact] - public async Task Can_depend_on_non_generic_options_when_only_one_context() - { - var serviceProvider = new ServiceCollection() - .AddDbContext() - .BuildServiceProvider(); - - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = serviceProvider.GetRequiredService(); - - Assert.True(await context.Customers.AnyAsync()); - } - - [Fact] - public async Task Can_depend_on_non_generic_options_when_only_one_context_with_default_service_provider() - { - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - await using var context = new NonGenericOptionsContext(new DbContextOptions()); - - Assert.True(await context.Customers.AnyAsync()); - } - - private class NonGenericOptionsContext(DbContextOptions options) : NorthwindContextBase(options) - { - private readonly DbContextOptions _options = options; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - Assert.Same(_options, optionsBuilder.Options); - - optionsBuilder.UseNpgsql(NpgsqlTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); - - Assert.NotSame(_options, optionsBuilder.Options); - } - } - - private class NorthwindContextBase : DbContext - { - protected NorthwindContextBase() - { - } - - protected NorthwindContextBase(DbContextOptions options) - : base(options) - { - } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public DbSet Customers { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity( - b => - { - b.HasKey(c => c.CustomerId); - b.ToTable("Customers"); - }); - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class Customer - { - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public string CustomerId { get; set; } - - // ReSharper disable once UnusedMember.Local - public string CompanyName { get; set; } - - // ReSharper disable once UnusedMember.Local - public string Fax { get; set; } - } - - #region Added for Npgsql - - [Fact] - public async Task Can_create_admin_connection_with_data_source() - { - await using var dataSource = NpgsqlDataSource.Create(NpgsqlTestStore.NorthwindConnectionString); - - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(dataSource, b => b.ApplyConfiguration()); - await using var context = new GeneralOptionsContext(optionsBuilder.Options); - - var relationalConnection = context.GetService(); - await using var adminConnection = relationalConnection.CreateAdminConnection(); - - Assert.Equal("postgres", new NpgsqlConnectionStringBuilder(adminConnection.ConnectionString).Database); - - await adminConnection.OpenAsync(CancellationToken.None); - } - - [Fact] - public async Task Can_create_admin_connection_with_connection_string() - { - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(NpgsqlTestStore.NorthwindConnectionString, b => b.ApplyConfiguration()); - await using var context = new GeneralOptionsContext(optionsBuilder.Options); - - var relationalConnection = context.GetService(); - await using var adminConnection = relationalConnection.CreateAdminConnection(); - - Assert.Equal("postgres", new NpgsqlConnectionStringBuilder(adminConnection.ConnectionString).Database); - - await adminConnection.OpenAsync(CancellationToken.None); - } - - [Fact] - public async Task Can_create_admin_connection_with_connection() - { - await using var connection = new NpgsqlConnection(NpgsqlTestStore.NorthwindConnectionString); - connection.Open(); - - await using var _ = await NpgsqlTestStore.GetNorthwindStoreAsync(); - - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(connection, b => b.ApplyConfiguration()); - await using var context = new GeneralOptionsContext(optionsBuilder.Options); - - var relationalConnection = context.GetService(); - await using var adminConnection = relationalConnection.CreateAdminConnection(); - - Assert.Equal("postgres", new NpgsqlConnectionStringBuilder(adminConnection.ConnectionString).Database); - - adminConnection.Open(); - } - - private class GeneralOptionsContext(DbContextOptions options) : NorthwindContextBase(options); - - #endregion -} diff --git a/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs deleted file mode 100644 index 0d69d9173c..0000000000 --- a/test/EFCore.PG.FunctionalTests/ConvertToProviderTypesNpgsqlTest.cs +++ /dev/null @@ -1,123 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class ConvertToProviderTypesNpgsqlTest : ConvertToProviderTypesTestBase< - ConvertToProviderTypesNpgsqlTest.ConvertToProviderTypesNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public ConvertToProviderTypesNpgsqlTest(ConvertToProviderTypesNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - 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); - // } - // } - - [ConditionalFact(Skip = "DateTimeOffset with non-zero offset, https://github.com/dotnet/efcore/issues/26068")] - 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 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 Task Can_insert_and_read_back_object_backed_data_types() - => Task.CompletedTask; - - public class ConvertToProviderTypesNpgsqlFixture : ConvertToProviderTypesFixtureBase - { - public override bool StrictEquality - => true; - - public override bool SupportsAnsi - => false; - - public override bool SupportsUnicodeToAnsiConversion - => false; - - public override bool SupportsLargeStringComparisons - => true; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - public override bool SupportsBinaryKeys - => true; - - public override bool SupportsDecimalComparisons - => true; - - public override DateTime DefaultDateTime - => new(); - - public override bool PreservesDateTimeKind - => false; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - - // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data - var objectBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - objectBackedDataTypes[nameof(ObjectBackedDataTypes.DateTimeOffset)] - = new DateTimeOffset(new DateTime(), TimeSpan.Zero); - - var nullableBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - nullableBackedDataTypes[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); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs deleted file mode 100644 index 15502c0106..0000000000 --- a/test/EFCore.PG.FunctionalTests/CustomConvertersNpgsqlTest.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class CustomConvertersNpgsqlTest(CustomConvertersNpgsqlTest.CustomConvertersNpgsqlFixture fixture) - : CustomConvertersTestBase(fixture) -{ - // Disabled: PostgreSQL is case-sensitive - 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 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 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 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 Task Can_query_using_any_data_type_nullable_shadow() - => Task.CompletedTask; - - public override void Value_conversion_on_enum_collection_contains() - => Assert.Contains( - CoreStrings.TranslationFailed("").Substring(47), - Assert.Throws(() => base.Value_conversion_on_enum_collection_contains()).Message); - - public class CustomConvertersNpgsqlFixture : CustomConvertersFixtureBase - { - public override bool StrictEquality - => true; - - public override bool SupportsAnsi - => false; - - public override bool SupportsUnicodeToAnsiConversion - => true; - - public override bool SupportsLargeStringComparisons - => true; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override bool SupportsBinaryKeys - => true; - - public override bool SupportsDecimalComparisons - => true; - - public override DateTime DefaultDateTime - => new(); - - public override bool PreservesDateTimeKind - => false; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.DateTime) - .HasColumnType("timestamp without time zone"); - - // We don't support DateTimeOffset with non-zero offset, so we need to override the seeding data - var objectBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - objectBackedDataTypes[nameof(ObjectBackedDataTypes.DateTimeOffset)] - = new DateTimeOffset(new DateTime(), TimeSpan.Zero); - - var nullableBackedDataTypes = modelBuilder.Entity().Metadata.GetSeedData().Single(); - nullableBackedDataTypes[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); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs deleted file mode 100644 index 57c3b7cd59..0000000000 --- a/test/EFCore.PG.FunctionalTests/DataAnnotationNpgsqlTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class DataAnnotationNpgsqlTest(DataAnnotationNpgsqlTest.DataAnnotationNpgsqlFixture fixture) - : DataAnnotationRelationalTestBase(fixture) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - protected override TestHelpers TestHelpers - => NpgsqlTestHelpers.Instance; - - public override Task StringLengthAttribute_throws_while_inserting_value_longer_than_max_length() - => Task.CompletedTask; // Npgsql does not support length - - public override Task TimestampAttribute_throws_if_value_in_database_changed() - => Task.CompletedTask; // 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 - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs deleted file mode 100644 index 9eaba3a495..0000000000 --- a/test/EFCore.PG.FunctionalTests/DataBindingNpgsqlTest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class DataBindingNpgsqlTest(F1BytesNpgsqlFixture fixture) : DataBindingTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs deleted file mode 100644 index 2d1650a6f1..0000000000 --- a/test/EFCore.PG.FunctionalTests/DesignTimeNpgsqlTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; - -namespace Microsoft.EntityFrameworkCore; - -public class DesignTimeNpgsqlTest(DesignTimeNpgsqlTest.DesignTimeNpgsqlFixture fixture) - : DesignTimeTestBase(fixture) -{ - protected override Assembly ProviderAssembly - => typeof(NpgsqlDesignTimeServices).Assembly; - - public class DesignTimeNpgsqlFixture : DesignTimeFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj deleted file mode 100644 index ae0c6c54dc..0000000000 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - Npgsql.EntityFrameworkCore.PostgreSQL.FunctionalTests - Microsoft.EntityFrameworkCore - true - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs deleted file mode 100644 index def3b20285..0000000000 --- a/test/EFCore.PG.FunctionalTests/EntitySplittingNpgsqlTest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class EntitySplittingNpgsqlTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) - : EntitySplittingTestBase(fixture, testOutputHelper) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs deleted file mode 100644 index 839f99b7c2..0000000000 --- a/test/EFCore.PG.FunctionalTests/F1NpgsqlFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; - -namespace Microsoft.EntityFrameworkCore; - -public class F1BytesNpgsqlFixture : F1NpgsqlFixtureBase -{ - protected override void BuildModelExternal(ModelBuilder modelBuilder) - { - base.BuildModelExternal(modelBuilder); - - modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); - modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); - modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); - modelBuilder.Entity().Property("Version").HasConversion(new ArrayStructuralComparer()); - modelBuilder.Entity() - .OwnsOne( - s => s.Details, eb => - { - eb.Property("Version").IsRowVersion().HasConversion(new ArrayStructuralComparer()); - }); - } - - private class BytesToUIntConverter() : ValueConverter( - bytes => BitConverter.ToUInt32(bytes), - num => BitConverter.GetBytes(num), - mappingHints: null); -} - -public class F1NpgsqlFixture : F1NpgsqlFixtureBase -{ - protected override void BuildModelExternal(ModelBuilder modelBuilder) - { - base.BuildModelExternal(modelBuilder); - - // TODO: This is a hack to work around, remove in 8.0 after https://github.com/dotnet/efcore/pull/29401 - modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); - modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); - modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); - modelBuilder.Entity().Property("Version").HasConversion((ValueConverter?)null); - modelBuilder.Entity() - .OwnsOne( - s => s.Details, eb => - { - eb.Property("Version").IsRowVersion().HasConversion((ValueConverter?)null); - }); - } -} - -public abstract class F1NpgsqlFixtureBase : F1RelationalFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override TestHelpers TestHelpers - => NpgsqlTestHelpers.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs deleted file mode 100644 index 32ae436825..0000000000 --- a/test/EFCore.PG.FunctionalTests/FieldMappingNpgsqlTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class FieldMappingNpgsqlTest(FieldMappingNpgsqlTest.FieldMappingNpgsqlFixture fixture) - : FieldMappingTestBase(fixture) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public class FieldMappingNpgsqlFixture : FieldMappingFixtureBase - { - protected override string StoreName { get; } = "FieldMapping"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs deleted file mode 100644 index 9b99503d59..0000000000 --- a/test/EFCore.PG.FunctionalTests/FindNpgsqlTest.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public abstract class FindNpgsqlTest : FindTestBase -{ - protected FindNpgsqlTest(FindNpgsqlFixture fixture) - : base(fixture) - { - fixture.TestSqlLoggerFactory.Clear(); - } - - public class FindNpgsqlTestSet(FindNpgsqlFixture fixture) : FindNpgsqlTest(fixture) - { - protected override TestFinder Finder { get; } = new FindViaSetFinder(); - } - - public class FindNpgsqlTestContext(FindNpgsqlFixture fixture) : FindNpgsqlTest(fixture) - { - protected override TestFinder Finder { get; } = new FindViaContextFinder(); - } - - public class FindNpgsqlTestNonGeneric(FindNpgsqlFixture fixture) : FindNpgsqlTest(fixture) - { - protected override TestFinder Finder { get; } = new FindViaNonGenericContextFinder(); - } - - public class FindNpgsqlFixture : FindFixtureBase - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs deleted file mode 100644 index 9c68c0d66f..0000000000 --- a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs +++ /dev/null @@ -1,580 +0,0 @@ -using System.Collections; -using System.Globalization; -using System.Numerics; -using NetTopologySuite; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore; - -public class JsonTypesNpgsqlTest(NonSharedFixture fixture) : JsonTypesRelationalTestBase(fixture) -{ - #region Nested collections (unsupported) - - // 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 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 - - // 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 - - // 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 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); - - // 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; - - // 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 Task Can_read_write_collection_of_DateOnly_JSON_values_npgsql() - => Can_read_and_write_JSON_value>( - nameof(DateOnlyCollectionType.DateOnly), - [ - 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 Task Can_read_write_collection_of_nullable_DateOnly_JSON_values() - => Can_read_and_write_JSON_value>( - nameof(NullableDateOnlyCollectionType.DateOnly), - [ - DateOnly.MinValue, - new(2023, 5, 29), - DateOnly.MaxValue, - null - ], - """{"Prop":["-infinity","2023-05-29","infinity",null]}""", - mappedCollection: true); - - #endregion DateOnly - - #region DateTime - - // 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("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), 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_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; - - // 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("""{"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), - [ - DateTime.MinValue, - new(2023, 5, 29, 10, 52, 47), - DateTime.MaxValue - ], - expected, - mappedCollection: true); - - 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), - [ - DateTime.MinValue, - null, - new(2023, 5, 29, 10, 52, 47), - DateTime.MaxValue - ], - """{"Prop":["-infinity",null,"2023-05-29T10:52:47","infinity"]}""", - mappedCollection: true); - - #endregion DateTime - - #region DateTimeOffset - - // 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.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); - - // 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; - - // 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 virtual Task Can_read_write_collection_of_DateTimeOffset_JSON_values_npgsql() - => Can_read_and_write_JSON_value>( - nameof(DateTimeOffsetCollectionType.DateTimeOffset), - [ - 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 virtual Task Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values_npgsql() - => Can_read_and_write_JSON_value>( - nameof(NullableDateTimeOffsetCollectionType.DateTimeOffset), - [ - 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); - - #endregion DateTimeOffset - - [ConditionalTheory] - [InlineData("12:34:56.123456+05:00", """{"Prop":"12:34:56.123456\u002B5"}""")] - public virtual void Can_read_write_timetz_JSON_values(string value, string json) - => Can_read_and_write_JSON_property_value( - b => b.HasColumnType("timetz"), - nameof(DateTimeOffsetType.DateTimeOffset), - DateTimeOffset.Parse(value), - json); - - [ConditionalTheory] - [InlineData(Mood.Happy, """{"Prop":"Happy"}""")] - [InlineData(Mood.Sad, """{"Prop":"Sad"}""")] - public virtual Task Can_read_write_pg_enum_JSON_values(Mood value, string json) - => Can_read_and_write_JSON_value( - nameof(EnumType.Mood), - value, - json); - - protected class EnumType - { - public Mood Mood { get; set; } - } - - public enum Mood - { - Happy, - Sad - } - - [ConditionalTheory] - [InlineData(new[] { 1, 2, 3 }, """{"Prop":[1,2,3]}""")] - [InlineData(new int[0], """{"Prop":[]}""")] - public virtual Task Can_read_write_array_JSON_values(int[] value, string json) - => Can_read_and_write_JSON_value( - nameof(ArrayType.Array), - value, - json, - mappedCollection: true); - - protected class ArrayType - { - public int[] Array { get; set; } = null!; - } - - [ConditionalFact] - 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.ThrowsAsync( - () => Can_read_and_write_JSON_value( - nameof(MultidimensionalArrayType.MultidimensionalArray), - new[,] { { 1, 2 }, { 3, 4 } }, - "")); - - protected class MultidimensionalArrayType - { - public int[,] MultidimensionalArray { get; set; } = null!; - } - - [ConditionalFact] - public virtual Task Can_read_write_BigInteger_JSON_values() - => Can_read_and_write_JSON_value( - nameof(BigIntegerType.BigInteger), - new BigInteger(ulong.MaxValue), - """{"Prop":"18446744073709551615"}"""); - - protected class BigIntegerType - { - public BigInteger BigInteger { get; set; } - } - - [ConditionalTheory] - [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 Task Can_read_write_BitArray_JSON_values(bool[] value, string json) - => Can_read_and_write_JSON_value( - nameof(BitArrayType.BitArray), - new BitArray(value), - json); - - protected class BitArrayType - { - public BitArray BitArray { get; set; } = null!; - } - - [ConditionalTheory] - [InlineData(1000, """{"Prop":"0/3E8"}""")] - [InlineData(0, """{"Prop":"0/0"}""")] - 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 ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; - - protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => serviceCollection.AddEntityFrameworkNpgsqlNetTopologySuite(); - - protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - // 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. - 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 deleted file mode 100644 index 998a0c5209..0000000000 --- a/test/EFCore.PG.FunctionalTests/KeysWithConvertersNpgsqlTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class KeysWithConvertersNpgsqlTest(KeysWithConvertersNpgsqlTest.KeysWithConvertersNpgsqlFixture fixture) - : KeysWithConvertersTestBase< - KeysWithConvertersNpgsqlTest.KeysWithConvertersNpgsqlFixture>(fixture) -{ - public class KeysWithConvertersNpgsqlFixture : KeysWithConvertersFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => builder.UseNpgsql(b => b.MinBatchSize(1)); - } -} diff --git a/test/EFCore.PG.FunctionalTests/LazyLoadProxyNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/LazyLoadProxyNpgsqlTest.cs deleted file mode 100644 index affdd7bf3f..0000000000 --- a/test/EFCore.PG.FunctionalTests/LazyLoadProxyNpgsqlTest.cs +++ /dev/null @@ -1,1512 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -// ReSharper disable once UnusedMember.Global -public class LazyLoadProxyNpgsqlTest : LazyLoadProxyRelationalTestBase -{ - public LazyLoadProxyNpgsqlTest(LoadNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - [ConditionalFact] // Requires MARS - public override void Top_level_projection_track_entities_before_passing_to_client_method() { } - - [ConditionalTheory(Skip = "Possibly requires MARS, investigate")] - public override void Lazy_load_one_to_one_reference_with_recursive_property(EntityState state) - => base.Lazy_load_one_to_one_reference_with_recursive_property(state); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - protected override void RecordLog() - => Sql = Fixture.TestSqlLoggerFactory.Sql; - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - private string Sql { get; set; } = null!; - - #region Expected JSON override - - // TODO: Tiny discrepancy in decimal representation (Charge: 1.0 instead of 1.00) - protected override string SerializedBlogs2 - => """ -{ - "$id": "1", - "$values": [ - { - "$id": "2", - "Id": 1, - "Writer": { - "$id": "3", - "FirstName": "firstNameWriter0", - "LastName": "lastNameWriter0", - "Alive": false, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "4", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "5", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "6", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "7", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "8", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "9", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "10", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Reader": { - "$id": "11", - "FirstName": "firstNameReader0", - "LastName": "lastNameReader0", - "Alive": false, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "12", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "13", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "14", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "15", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "16", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "17", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "18", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Host": { - "$id": "19", - "HostName": "127.0.0.1", - "Rating": 0, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "20", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "21", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "22", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "23", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "24", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "25", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "26", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "27", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "28", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "29", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "30", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "31", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "32", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "33", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - { - "$id": "34", - "Id": 2, - "Writer": { - "$id": "35", - "FirstName": "firstNameWriter1", - "LastName": "lastNameWriter1", - "Alive": false, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "36", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "37", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "38", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "39", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "40", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "41", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "42", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Reader": { - "$id": "43", - "FirstName": "firstNameReader1", - "LastName": "lastNameReader1", - "Alive": false, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "44", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "45", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "46", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "47", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "48", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "49", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "50", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Host": { - "$id": "51", - "HostName": "127.0.0.2", - "Rating": 0, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "52", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "53", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "54", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "55", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "56", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "57", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "58", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "59", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "60", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "61", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "62", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "63", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "64", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "65", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - { - "$id": "66", - "Id": 3, - "Writer": { - "$id": "67", - "FirstName": "firstNameWriter2", - "LastName": "lastNameWriter2", - "Alive": false, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "68", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "69", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "70", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "71", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "72", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "73", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "74", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Reader": { - "$id": "75", - "FirstName": "firstNameReader2", - "LastName": "lastNameReader2", - "Alive": false, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "76", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "77", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "78", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "79", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "80", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "81", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "82", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Host": { - "$id": "83", - "HostName": "127.0.0.3", - "Rating": 0, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "84", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "85", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "86", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "87", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "88", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "89", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "90", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - }, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "91", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "92", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "93", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "$id": "94", - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "$id": "95", - "Name": "M1", - "Rating": 7, - "Tag": { - "$id": "96", - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "$id": "97", - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - } - } - ] -} -"""; - - protected override string SerializedBlogs1 - => """ -[ - { - "Writer": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "FirstName": "firstNameWriter0", - "LastName": "lastNameWriter0", - "Alive": false - }, - "Reader": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "FirstName": "firstNameReader0", - "LastName": "lastNameReader0", - "Alive": false - }, - "Host": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "HostName": "127.0.0.1", - "Rating": 0.0 - }, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Id": 1 - }, - { - "Writer": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "FirstName": "firstNameWriter1", - "LastName": "lastNameWriter1", - "Alive": false - }, - "Reader": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "FirstName": "firstNameReader1", - "LastName": "lastNameReader1", - "Alive": false - }, - "Host": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "HostName": "127.0.0.2", - "Rating": 0.0 - }, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Id": 2 - }, - { - "Writer": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "FirstName": "firstNameWriter2", - "LastName": "lastNameWriter2", - "Alive": false - }, - "Reader": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "FirstName": "firstNameReader2", - "LastName": "lastNameReader2", - "Alive": false - }, - "Host": { - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "HostName": "127.0.0.3", - "Rating": 0.0 - }, - "Culture": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Milk": { - "Species": "S1", - "Subspecies": null, - "Rating": 8, - "Validation": false, - "Manufacturer": { - "Name": "M1", - "Rating": 7, - "Tag": { - "Text": "Ta2" - }, - "Tog": { - "Text": "To2" - } - }, - "License": { - "Title": "Ti1", - "Charge": 1.0, - "Tag": { - "Text": "Ta1" - }, - "Tog": { - "Text": "To1" - } - } - }, - "Id": 3 - } -] -"""; - - #endregion Expected JSON override - - public class LoadNpgsqlFixture : LoadRelationalFixtureBase - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(q => q.Birthday).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(q => q.Birthday).HasColumnType("timestamp without time zone"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/LoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/LoadNpgsqlTest.cs deleted file mode 100644 index d383c72d51..0000000000 --- a/test/EFCore.PG.FunctionalTests/LoadNpgsqlTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -// ReSharper disable once UnusedMember.Global -public class LoadNpgsqlTest : LoadTestBase -{ - public LoadNpgsqlTest(LoadNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - protected override void RecordLog() - => Sql = Fixture.TestSqlLoggerFactory.Sql; - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - private string Sql { get; set; } = null!; - - public class LoadNpgsqlFixture : LoadFixtureBase - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs deleted file mode 100644 index 06380e3b57..0000000000 --- a/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Microsoft.EntityFrameworkCore; - -public class LoggingNpgsqlTest : LoggingRelationalTestBase -{ - [Fact] - public void Logs_context_initialization_admin_database() - => Assert.Equal( - ExpectedMessage($"AdminDatabase=foo {DefaultOptions}"), - ActualMessage(s => CreateOptionsBuilder(s, b => ((NpgsqlDbContextOptionsBuilder)b).UseAdminDatabase("foo")))); - - [Fact] - public void Logs_context_initialization_postgres_version() - => Assert.Equal( - 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( - ExpectedMessage($"ProvideClientCertificatesCallback {DefaultOptions}"), - ActualMessage( - s => CreateOptionsBuilder( - s, b => ((NpgsqlDbContextOptionsBuilder)b).ProvideClientCertificatesCallback(_ => { })))); - - [Fact] - public void Logs_context_initialization_provide_password_callback() - => Assert.Equal( - ExpectedMessage($"ProvidePasswordCallback {DefaultOptions}"), - ActualMessage( - s => CreateOptionsBuilder( - s, b => ((NpgsqlDbContextOptionsBuilder)b).ProvidePasswordCallback((_, _, _, _) => "password")))); - - [Fact] - public void Logs_context_initialization_remote_certificate_validation_callback() - => Assert.Equal( - ExpectedMessage($"RemoteCertificateValidationCallback {DefaultOptions}"), - ActualMessage( - 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() - => Assert.Equal( - ExpectedMessage($"ReverseNullOrdering {DefaultOptions}"), - ActualMessage(s => CreateOptionsBuilder(s, b => ((NpgsqlDbContextOptionsBuilder)b).ReverseNullOrdering()))); - - [Fact] - public void Logs_context_initialization_user_range_definitions() - => Assert.Equal( - ExpectedMessage($"UserRangeDefinitions=[{typeof(int)}=>int4range] " + DefaultOptions), - ActualMessage(s => CreateOptionsBuilder(s, b => ((NpgsqlDbContextOptionsBuilder)b).MapRange("int4range")))); - - protected override DbContextOptionsBuilder CreateOptionsBuilder( - IServiceCollection services, - Action> relationalAction) - => new DbContextOptionsBuilder() - .UseInternalServiceProvider(services.AddEntityFrameworkNpgsql().BuildServiceProvider()) - .UseNpgsql("Data Source=LoggingNpgsqlTest.db", relationalAction); - - protected override TestLogger CreateTestLogger() - => new TestLogger(); - - protected override string ProviderName - => "Npgsql.EntityFrameworkCore.PostgreSQL"; - - protected override string ProviderVersion - => typeof(NpgsqlOptionsExtension).Assembly - .GetCustomAttribute()?.InformationalVersion!; -} diff --git a/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs deleted file mode 100644 index 4280f23638..0000000000 --- a/test/EFCore.PG.FunctionalTests/ManyToManyFieldsLoadNpgsqlTest.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ManyToManyFieldsModel; - -namespace Microsoft.EntityFrameworkCore; - -public class ManyToManyFieldsLoadNpgsqlTest(ManyToManyFieldsLoadNpgsqlTest.ManyToManyFieldsLoadNpgsqlFixture fixture) - : ManyToManyFieldsLoadTestBase(fixture) -{ - public class ManyToManyFieldsLoadNpgsqlFixture : ManyToManyFieldsLoadFixtureBase, ITestSqlLoggerFactory - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity() - .Property(e => e.Key3) - .HasColumnType("timestamp without time zone"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasColumnType("timestamp without time zone") - .HasDefaultValueSql("CURRENT_TIMESTAMP"); - - modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") - .IndexerProperty("Payload") - .HasDefaultValue("Generated"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasDefaultValue("Generated"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs deleted file mode 100644 index c541846353..0000000000 --- a/test/EFCore.PG.FunctionalTests/ManyToManyLoadNpgsqlTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; - -namespace Microsoft.EntityFrameworkCore; - -public class ManyToManyLoadNpgsqlTest(ManyToManyLoadNpgsqlTest.ManyToManyLoadNpgsqlFixture fixture) - : ManyToManyLoadTestBase(fixture) -{ - public class ManyToManyLoadNpgsqlFixture : ManyToManyLoadFixtureBase, ITestSqlLoggerFactory - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity() - .Property(e => e.Key3) - .HasColumnType("timestamp without time zone"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasColumnType("timestamp without time zone") - .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); - - modelBuilder - .Entity() - .Property(e => e.Key3) - .HasColumnType("timestamp without time zone") - .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasColumnType("timestamp without time zone") - .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); - - modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") - .IndexerProperty("Payload") - .HasDefaultValue("Generated"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasDefaultValue("Generated"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs deleted file mode 100644 index 7d14f32bc4..0000000000 --- a/test/EFCore.PG.FunctionalTests/ManyToManyTrackingNpgsqlTest.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; - -namespace Microsoft.EntityFrameworkCore; - -public class ManyToManyTrackingNpgsqlTest(ManyToManyTrackingNpgsqlTest.ManyToManyTrackingNpgsqlFixture fixture) - : ManyToManyTrackingRelationalTestBase< - ManyToManyTrackingNpgsqlTest.ManyToManyTrackingNpgsqlFixture>(fixture) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - 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); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasColumnType("timestamp without time zone") - .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); - - modelBuilder - .SharedTypeEntity>("JoinOneToThreePayloadFullShared") - .IndexerProperty("Payload") - .HasDefaultValue("Generated"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasDefaultValue("Generated"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasColumnType("timestamp without time zone") - .HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'"); - - modelBuilder - .SharedTypeEntity>("UnidirectionalJoinOneToThreePayloadFullShared") - .IndexerProperty("Payload") - .HasDefaultValue("Generated"); - - modelBuilder - .Entity() - .Property(e => e.Payload) - .HasDefaultValue("Generated"); - - // Additional Npgsql-specific config (for timestamp without time zone) - modelBuilder - .Entity() - .Property(e => e.Key3) - .HasColumnType("timestamp without time zone"); - - modelBuilder.Entity() - .Property(e => e.Key3) - .HasColumnType("timestamp without time zone"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs deleted file mode 100644 index 48e9f5bd7d..0000000000 --- a/test/EFCore.PG.FunctionalTests/MaterializationInterceptionNpgsqlTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class MaterializationInterceptionNpgsqlTest(NonSharedFixture fixture) : - MaterializationInterceptionTestBase(fixture) -{ - public class NpgsqlLibraryContext(DbContextOptions options) : LibraryContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity().OwnsMany(e => e.Settings); - - // #2548 - // modelBuilder.Entity().OwnsMany(e => e.Settings, b => b.ToJson()); - } - } - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs b/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs deleted file mode 100644 index 2350593abe..0000000000 --- a/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderGenericTest.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.ModelBuilding; - -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 NpgsqlGenericComplexCollection(NpgsqlModelBuilderFixture fixture) : NpgsqlComplexCollection(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 deleted file mode 100644 index d9d6de800e..0000000000 --- a/test/EFCore.PG.FunctionalTests/ModelBuilding/NpgsqlModelBuilderTestBase.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.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 NpgsqlComplexCollection(NpgsqlModelBuilderFixture fixture) - : RelationalComplexCollectionTestBase(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/ModelBuilding101NpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/ModelBuilding101NpgsqlTest.cs deleted file mode 100644 index ab5f152686..0000000000 --- a/test/EFCore.PG.FunctionalTests/ModelBuilding101NpgsqlTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class ModelBuilding101NpgsqlTest : ModelBuilding101RelationalTestBase -{ - protected override DbContextOptionsBuilder ConfigureContext(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(); -} diff --git a/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs deleted file mode 100644 index 3707ea498f..0000000000 --- a/test/EFCore.PG.FunctionalTests/MonsterFixupChangedChangingNpgsqlTest.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class MonsterFixupChangedChangingNpgsqlTest(MonsterFixupChangedChangingNpgsqlTest.MonsterFixupChangedChangingNpgsqlFixture fixture) - : MonsterFixupTestBase(fixture) -{ - public class MonsterFixupChangedChangingNpgsqlFixture : MonsterFixupChangedChangingFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating( - ModelBuilder builder) - { - base.OnModelCreating(builder); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - foreach (var property in builder.Model.GetEntityTypes() - .SelectMany( - e => e.GetProperties().Where( - p => - p.ClrType == typeof(DateTime) || p.ClrType == typeof(DateTime?)))) - { - property.SetColumnType("timestamp without time zone"); - } - - builder.Entity().Property(e => e.MessageId).UseSerialColumn(); - builder.Entity().Property(e => e.PhotoId).UseSerialColumn(); - builder.Entity().Property(e => e.ReviewId).UseSerialColumn(); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs deleted file mode 100644 index e3cc6f3830..0000000000 --- a/test/EFCore.PG.FunctionalTests/MusicStoreNpgsqlTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.MusicStore; - -namespace Microsoft.EntityFrameworkCore; - -public class MusicStoreNpgsqlTest(MusicStoreNpgsqlTest.MusicStoreNpgsqlFixture fixture) - : MusicStoreTestBase(fixture) -{ - public class MusicStoreNpgsqlFixture : MusicStoreFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity().Property(s => s.DateCreated).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(s => s.OrderDate).HasColumnType("timestamp without time zone"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs deleted file mode 100644 index 62e30d7ee7..0000000000 --- a/test/EFCore.PG.FunctionalTests/NotificationEntitiesNpgsqlTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class NotificationEntitiesNpgsqlTest(NotificationEntitiesNpgsqlTest.NotificationEntitiesNpgsqlFixture fixture) - : NotificationEntitiesTestBase(fixture) -{ - public class NotificationEntitiesNpgsqlFixture : NotificationEntitiesFixtureBase - { - protected override string StoreName { get; } = "NotificationEntities"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs deleted file mode 100644 index 4479fdb828..0000000000 --- a/test/EFCore.PG.FunctionalTests/NpgsqlApiConsistencyTest.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -public class NpgsqlApiConsistencyTest(NpgsqlApiConsistencyTest.NpgsqlApiConsistencyFixture fixture) - : ApiConsistencyTestBase(fixture) -{ - protected override void AddServices(ServiceCollection serviceCollection) - => serviceCollection.AddEntityFrameworkNpgsql(); - - protected override Assembly TargetAssembly - => typeof(NpgsqlRelationalConnection).Assembly; - - public class NpgsqlApiConsistencyFixture : ApiConsistencyFixtureBase - { - public override HashSet FluentApiTypes { get; } = - [ - typeof(NpgsqlDbContextOptionsBuilder), - typeof(NpgsqlDbContextOptionsBuilderExtensions), - typeof(NpgsqlMigrationBuilderExtensions), - typeof(NpgsqlIndexBuilderExtensions), - typeof(NpgsqlModelBuilderExtensions), - typeof(NpgsqlPropertyBuilderExtensions), - typeof(NpgsqlEntityTypeBuilderExtensions), - typeof(NpgsqlServiceCollectionExtensions) - ]; - - public override HashSet UnmatchedMetadataMethods { get; } = - [ - typeof(NpgsqlPropertyBuilderExtensions).GetMethod( - nameof(NpgsqlPropertyBuilderExtensions.IsGeneratedTsVectorColumn), - [typeof(PropertyBuilder), typeof(string), typeof(string[])]) - ]; - - public override - Dictionary MetadataExtensionTypes { get; } - = new() - { - { - typeof(IReadOnlyModel), ( - typeof(NpgsqlModelExtensions), - typeof(NpgsqlModelExtensions), - typeof(NpgsqlModelExtensions), - typeof(NpgsqlModelBuilderExtensions), - null - ) - }, - { - typeof(IReadOnlyEntityType), ( - typeof(NpgsqlEntityTypeExtensions), - typeof(NpgsqlEntityTypeExtensions), - typeof(NpgsqlEntityTypeExtensions), - typeof(NpgsqlEntityTypeBuilderExtensions), - null - ) - }, - { - typeof(IReadOnlyProperty), ( - typeof(NpgsqlPropertyExtensions), - typeof(NpgsqlPropertyExtensions), - typeof(NpgsqlPropertyExtensions), - typeof(NpgsqlPropertyBuilderExtensions), - null - ) - }, - { - typeof(IReadOnlyIndex), ( - typeof(NpgsqlIndexExtensions), - typeof(NpgsqlIndexExtensions), - typeof(NpgsqlIndexExtensions), - typeof(NpgsqlIndexBuilderExtensions), - null - ) - } - }; - - public override HashSet MetadataMethodExceptions { get; } = - [ - typeof(NpgsqlEntityTypeExtensions).GetRuntimeMethod( - nameof(NpgsqlEntityTypeExtensions.SetStorageParameter), - [typeof(IConventionEntityType), typeof(string), typeof(object), typeof(bool)]) - ]; - } -} diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs deleted file mode 100644 index 66e8b515b3..0000000000 --- a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class NpgsqlComplianceTest : RelationalComplianceTestBase -{ - protected override ICollection IgnoredTestBases { get; } = new HashSet - { - // Not implemented - typeof(CompiledModelTestBase), typeof(CompiledModelRelationalTestBase), // #3087 - typeof(FromSqlSprocQueryTestBase<>), - typeof(UdfDbFunctionTestBase<>), - typeof(UpdateSqlGeneratorTestBase), - - // Disabled - typeof(GraphUpdatesTestBase<>), - typeof(ProxyGraphUpdatesTestBase<>), - typeof(OperatorsProceduralQueryTestBase), - }; - - protected override Assembly TargetAssembly { get; } = typeof(NpgsqlComplianceTest).Assembly; -} diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs deleted file mode 100644 index e026c790a6..0000000000 --- a/test/EFCore.PG.FunctionalTests/NpgsqlDatabaseCreatorTest.cs +++ /dev/null @@ -1,648 +0,0 @@ -using System.Data; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable HeapView.CanAvoidClosure -// ReSharper disable MethodHasAsyncOverload - -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -public class NpgsqlDatabaseCreatorExistsTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true, true, false)] - [InlineData(false, false, false)] - [InlineData(true, true, true)] - [InlineData(false, false, true)] - public async Task Returns_false_when_database_does_not_exist(bool async, bool ambientTransaction, bool useCanConnect) - { - await using var testDatabase = NpgsqlTestStore.Create("NonExisting"); - await using var context = new BloggingContext(testDatabase); - var creator = GetDatabaseCreator(context); - - await context.Database.CreateExecutionStrategy().ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - if (useCanConnect) - { - Assert.False(async ? await creator.CanConnectAsync() : creator.CanConnect()); - } - else - { - Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); - } - } - }); - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - } - - [ConditionalTheory] - [InlineData(true, false, false)] - [InlineData(false, true, false)] - [InlineData(true, false, true)] - [InlineData(false, true, true)] - public async Task Returns_true_when_database_exists(bool async, bool ambientTransaction, bool useCanConnect) - { - await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("ExistingBlogging"); - await using var context = new BloggingContext(testDatabase); - var creator = GetDatabaseCreator(context); - - await context.Database.CreateExecutionStrategy().ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - if (useCanConnect) - { - Assert.True(async ? await creator.CanConnectAsync() : creator.CanConnect()); - } - else - { - Assert.True(async ? await creator.ExistsAsync() : creator.Exists()); - } - } - }); - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - } -} - -public class NpgsqlDatabaseCreatorEnsureDeletedTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true, true, true)] - [InlineData(false, false, true)] - [InlineData(true, false, false)] - [InlineData(false, true, false)] - public async Task Deletes_database(bool async, bool open, bool ambientTransaction) - { - await using var testDatabase = await NpgsqlTestStore.CreateInitializedAsync("EnsureDeleteBlogging"); - if (!open) - { - testDatabase.CloseConnection(); - } - - await using var context = new BloggingContext(testDatabase); - var creator = GetDatabaseCreator(context); - - Assert.True(async ? await creator.ExistsAsync() : creator.Exists()); - - await GetExecutionStrategy(testDatabase).ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - if (async) - { - Assert.True(await context.Database.EnsureDeletedAsync()); - } - else - { - Assert.True(context.Database.EnsureDeleted()); - } - } - }); - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - - Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - private static async Task Noop_when_database_does_not_exist_test(bool async) - { - await using var testDatabase = NpgsqlTestStore.Create("NonExisting"); - await using var context = new BloggingContext(testDatabase); - var creator = GetDatabaseCreator(context); - - Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); - - if (async) - { - Assert.False(await creator.EnsureDeletedAsync()); - } - else - { - Assert.False(creator.EnsureDeleted()); - } - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - - Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - } -} - -public class NpgsqlDatabaseCreatorEnsureCreatedTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true, true)] - [InlineData(false, false)] - public Task Creates_schema_in_existing_database(bool async, bool ambientTransaction) - => Creates_physical_database_and_schema_test((true, async, ambientTransaction)); - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, true)] - public Task Creates_physical_database_and_schema(bool async, bool ambientTransaction) - => Creates_new_physical_database_and_schema_test(async, ambientTransaction); - - private static Task Creates_new_physical_database_and_schema_test(bool async, bool ambientTransaction) - => Creates_physical_database_and_schema_test((false, async, ambientTransaction)); - - private static async Task Creates_physical_database_and_schema_test( - (bool CreateDatabase, bool Async, bool ambientTransaction) options) - { - var (createDatabase, async, ambientTransaction) = options; - await using var testDatabase = NpgsqlTestStore.Create("EnsureCreatedTest"); - await using var context = new BloggingContext(testDatabase); - if (createDatabase) - { - await testDatabase.InitializeAsync(null, (Func)null); - } - else - { - await testDatabase.DeleteDatabaseAsync(); - } - - var creator = GetDatabaseCreator(context); - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - - using (CreateTransactionScope(ambientTransaction)) - { - if (async) - { - Assert.True(await creator.EnsureCreatedAsync()); - } - else - { - Assert.True(creator.EnsureCreated()); - } - } - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - - if (testDatabase.ConnectionState != ConnectionState.Open) - { - await testDatabase.OpenConnectionAsync(); - } - - var tables = testDatabase.Query( - "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND NOT TABLE_NAME LIKE ANY ('{pg_%,sql_%}')") - .ToList(); - Assert.Single(tables); - Assert.Equal("Blogs", tables.Single()); - - var columns = testDatabase.Query( - "SELECT TABLE_NAME || '.' || COLUMN_NAME || ' (' || DATA_TYPE || ')' FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Blogs' ORDER BY TABLE_NAME, COLUMN_NAME") - .ToArray(); - Assert.Equal(14, columns.Length); - - Assert.Equal( - new[] { - "Blogs.AndChew (bytea)", - "Blogs.AndRow (bytea)", - "Blogs.Cheese (text)", - "Blogs.ErMilan (integer)", - "Blogs.Fuse (smallint)", - "Blogs.George (boolean)", - "Blogs.Key1 (text)", - "Blogs.Key2 (bytea)", - "Blogs.NotFigTime (timestamp with time zone)", - "Blogs.On (real)", - "Blogs.OrNothing (double precision)", - "Blogs.TheGu (uuid)", - "Blogs.ToEat (smallint)", - "Blogs.WayRound (bigint)" - }, - columns); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public async Task Noop_when_database_exists_and_has_schema(bool async) - { - await using var testDatabase = await NpgsqlTestStore.CreateInitializedAsync("InitializedBlogging"); - await using var context = new BloggingContext(testDatabase); - context.Database.EnsureCreatedResiliently(); - - if (async) - { - Assert.False(await context.Database.EnsureCreatedResilientlyAsync()); - } - else - { - Assert.False(context.Database.EnsureCreatedResiliently()); - } - - Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); - } -} - -public class NpgsqlDatabaseCreatorHasTablesTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public async Task Throws_when_database_does_not_exist(bool async) - { - await using var testDatabase = NpgsqlTestStore.GetOrCreate("NonExisting"); - var databaseCreator = GetDatabaseCreator(testDatabase); - await databaseCreator.ExecutionStrategy.ExecuteAsync( - databaseCreator, - async creator => - { - var errorNumber = async - ? (await Assert.ThrowsAsync(() => creator.HasTablesAsyncBase())).SqlState - : Assert.Throws(() => creator.HasTablesBase()).SqlState; - - Assert.Equal(PostgresErrorCodes.InvalidCatalogName, errorNumber); - }); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, true)] - public async Task Returns_false_when_database_exists_but_has_no_tables(bool async, bool ambientTransaction) - { - await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("Empty"); - var creator = GetDatabaseCreator(testDatabase); - - await GetExecutionStrategy(testDatabase).ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - Assert.False(async ? await creator.HasTablesAsyncBase() : creator.HasTablesBase()); - } - }); - } - - [ConditionalTheory] - [InlineData(true, true)] - [InlineData(false, false)] - public async Task Returns_true_when_database_exists_and_has_any_tables(bool async, bool ambientTransaction) - { - await using var testDatabase = await NpgsqlTestStore.GetOrCreate("ExistingTables") - .InitializeNpgsqlAsync(null, t => new BloggingContext(t), null); - var creator = GetDatabaseCreator(testDatabase); - - await GetExecutionStrategy(testDatabase).ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - Assert.True(async ? await creator.HasTablesAsyncBase() : creator.HasTablesBase()); - } - }); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, true)] - [RequiresPostgis] - public async Task Returns_false_when_database_exists_and_has_only_postgis_tables(bool async, bool ambientTransaction) - { - await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("Empty"); - testDatabase.ExecuteNonQuery("CREATE EXTENSION IF NOT EXISTS postgis"); - - var creator = GetDatabaseCreator(testDatabase); - - await GetExecutionStrategy(testDatabase).ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - Assert.False(async ? await creator.HasTablesAsyncBase() : creator.HasTablesBase()); - } - }); - } -} - -public class NpgsqlDatabaseCreatorDeleteTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true, true)] - [InlineData(false, false)] - public static async Task Deletes_database(bool async, bool ambientTransaction) - { - await using var testDatabase = await NpgsqlTestStore.CreateInitializedAsync("DeleteBlogging"); - testDatabase.CloseConnection(); - - var creator = GetDatabaseCreator(testDatabase); - - Assert.True(async ? await creator.ExistsAsync() : creator.Exists()); - - using (CreateTransactionScope(ambientTransaction)) - { - if (async) - { - await creator.DeleteAsync(); - } - else - { - creator.Delete(); - } - } - - Assert.False(async ? await creator.ExistsAsync() : creator.Exists()); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public async Task Throws_when_database_does_not_exist(bool async) - { - await using var testDatabase = NpgsqlTestStore.GetOrCreate("NonExistingBlogging"); - var creator = GetDatabaseCreator(testDatabase); - - if (async) - { - await Assert.ThrowsAsync(() => creator.DeleteAsync()); - } - else - { - Assert.Throws(() => creator.Delete()); - } - } -} - -public class NpgsqlDatabaseCreatorCreateTablesTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true, true)] - [InlineData(false, false)] - public async Task Creates_schema_in_existing_database_test(bool async, bool ambientTransaction) - { - await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("ExistingBlogging" + (async ? "Async" : "")); - await using var context = new BloggingContext(testDatabase); - var creator = GetDatabaseCreator(context); - - using (CreateTransactionScope(ambientTransaction)) - { - if (async) - { - await creator.CreateTablesAsync(); - } - else - { - creator.CreateTables(); - } - } - - if (testDatabase.ConnectionState != ConnectionState.Open) - { - await testDatabase.OpenConnectionAsync(); - } - - var tables = (await testDatabase.QueryAsync( - "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND NOT TABLE_NAME LIKE ANY ('{pg_%,sql_%}')")) - .ToList(); - Assert.Single(tables); - Assert.Equal("Blogs", tables.Single()); - - var columns = (await testDatabase.QueryAsync( - "SELECT TABLE_NAME || '.' || COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Blogs'")).ToList(); - Assert.Equal(14, columns.Count); - Assert.Contains(columns, c => c == "Blogs.Key1"); - Assert.Contains(columns, c => c == "Blogs.Key2"); - Assert.Contains(columns, c => c == "Blogs.Cheese"); - Assert.Contains(columns, c => c == "Blogs.ErMilan"); - Assert.Contains(columns, c => c == "Blogs.George"); - Assert.Contains(columns, c => c == "Blogs.TheGu"); - Assert.Contains(columns, c => c == "Blogs.NotFigTime"); - Assert.Contains(columns, c => c == "Blogs.ToEat"); - Assert.Contains(columns, c => c == "Blogs.OrNothing"); - Assert.Contains(columns, c => c == "Blogs.Fuse"); - Assert.Contains(columns, c => c == "Blogs.WayRound"); - Assert.Contains(columns, c => c == "Blogs.On"); - Assert.Contains(columns, c => c == "Blogs.AndChew"); - Assert.Contains(columns, c => c == "Blogs.AndRow"); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public async Task Throws_if_database_does_not_exist(bool async) - { - await using var testDatabase = NpgsqlTestStore.GetOrCreate("NonExisting"); - var creator = GetDatabaseCreator(testDatabase); - - var errorNumber - = async - ? (await Assert.ThrowsAsync(() => creator.CreateTablesAsync())).SqlState - : Assert.Throws(() => creator.CreateTables()).SqlState; - - Assert.Equal(PostgresErrorCodes.InvalidCatalogName, errorNumber); - } - - [ConditionalFact] - public void GenerateCreateScript_works() - { - using var context = new BloggingContext("Data Source=foo"); - var script = context.Database.GenerateCreateScript(); - Assert.Equal( - """CREATE TABLE "Blogs" (""" - + _eol - + """ "Key1" text NOT NULL,""" - + _eol - + """ "Key2" bytea NOT NULL,""" - + _eol - + """ "Cheese" text,""" - + _eol - + """ "ErMilan" integer NOT NULL,""" - + _eol - + """ "George" boolean NOT NULL,""" - + _eol - + """ "TheGu" uuid NOT NULL,""" - + _eol - + """ "NotFigTime" timestamp with time zone NOT NULL,""" - + _eol - + """ "ToEat" smallint NOT NULL,""" - + _eol - + """ "OrNothing" double precision NOT NULL,""" - + _eol - + """ "Fuse" smallint NOT NULL,""" - + _eol - + """ "WayRound" bigint NOT NULL,""" - + _eol - + """ "On" real NOT NULL,""" - + _eol - + """ "AndChew" bytea,""" - + _eol - + """ "AndRow" bytea,""" - + _eol - + """ CONSTRAINT "PK_Blogs" PRIMARY KEY ("Key1", "Key2")""" - + _eol - + ");" - + _eol - + _eol - + _eol, - script); - } - - private static readonly string _eol = Environment.NewLine; -} - -public class NpgsqlDatabaseCreatorCreateTest : NpgsqlDatabaseCreatorTest -{ - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, true)] - public async Task Creates_physical_database_but_not_tables(bool async, bool ambientTransaction) - { - await using var testDatabase = NpgsqlTestStore.GetOrCreate("CreateTest"); - var creator = GetDatabaseCreator(testDatabase); - - creator.EnsureDeleted(); - - await GetExecutionStrategy(testDatabase).ExecuteAsync( - async () => - { - using (CreateTransactionScope(ambientTransaction)) - { - if (async) - { - await creator.CreateAsync(); - } - else - { - creator.Create(); - } - } - }); - - Assert.True(creator.Exists()); - - if (testDatabase.ConnectionState != ConnectionState.Open) - { - await testDatabase.OpenConnectionAsync(); - } - - Assert.Empty( - await testDatabase.QueryAsync( - "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND NOT TABLE_NAME LIKE ANY ('{pg_%,sql_%}')")); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public async Task Throws_if_database_already_exists(bool async) - { - await using var testDatabase = await NpgsqlTestStore.GetOrCreateInitializedAsync("ExistingBlogging"); - var creator = GetDatabaseCreator(testDatabase); - - var ex = async - ? await Assert.ThrowsAsync(() => creator.CreateAsync()) - : Assert.Throws(() => creator.Create()); - Assert.Equal(PostgresErrorCodes.DuplicateDatabase, ex.SqlState); - } -} - -// When database creation/drop happens in parallel, there seem to be some deadlocks on the PostgreSQL side -// which make the tests run significantly slower. This makes all the test suites run in serial. -[Collection("NpgsqlDatabaseCreatorTest")] -public class NpgsqlDatabaseCreatorTest -{ - protected static IDisposable CreateTransactionScope(bool useTransaction) - => TestStore.CreateTransactionScope(useTransaction); - - protected static TestDatabaseCreator GetDatabaseCreator(NpgsqlTestStore testStore) - => GetDatabaseCreator(testStore.ConnectionString); - - protected static TestDatabaseCreator GetDatabaseCreator(string connectionString) - => GetDatabaseCreator(new BloggingContext(connectionString)); - - protected static TestDatabaseCreator GetDatabaseCreator(BloggingContext context) - => (TestDatabaseCreator)context.GetService(); - - protected static IExecutionStrategy GetExecutionStrategy(NpgsqlTestStore testStore) - => new BloggingContext(testStore).GetService().Create(); - - // ReSharper disable once ClassNeverInstantiated.Local - private class TestNpgsqlExecutionStrategyFactory(ExecutionStrategyDependencies dependencies) - : NpgsqlExecutionStrategyFactory(dependencies) - { - protected override IExecutionStrategy CreateDefaultStrategy(ExecutionStrategyDependencies dependencies) - => new NonRetryingExecutionStrategy(dependencies); - } - - private static IServiceProvider CreateServiceProvider() - => new ServiceCollection() - .AddEntityFrameworkNpgsql() - .AddScoped() - .AddScoped() - .BuildServiceProvider(); - - protected class BloggingContext(string connectionString) : DbContext - { - public BloggingContext(NpgsqlTestStore testStore) - : this(testStore.ConnectionString) - { - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .UseNpgsql( - connectionString, b => b - .ApplyConfiguration() - .SetPostgresVersion(TestEnvironment.PostgresVersion)) - .UseInternalServiceProvider(CreateServiceProvider()); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity( - b => - { - b.HasKey( - e => new { e.Key1, e.Key2 }); - b.Property(e => e.AndRow).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate(); - }); - - public DbSet Blogs { get; set; } - } - - public class Blog - { - public string Key1 { get; set; } - public byte[] Key2 { get; set; } - public string Cheese { get; set; } - public int ErMilan { get; set; } - public bool George { get; set; } - public Guid TheGu { get; set; } - public DateTime NotFigTime { get; set; } - public byte ToEat { get; set; } - public double OrNothing { get; set; } - public short Fuse { get; set; } - public long WayRound { get; set; } - public float On { get; set; } - public byte[] AndChew { get; set; } - public byte[] AndRow { get; set; } - } - - public class TestDatabaseCreator( - RelationalDatabaseCreatorDependencies dependencies, - INpgsqlRelationalConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder) - : NpgsqlDatabaseCreator(dependencies, connection, rawSqlCommandBuilder) - { - public bool HasTablesBase() - => HasTables(); - - public Task HasTablesAsyncBase(CancellationToken cancellationToken = default) - => HasTablesAsync(cancellationToken); - - public IExecutionStrategy ExecutionStrategy - => Dependencies.ExecutionStrategy; - } -} diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/NpgsqlFixture.cs deleted file mode 100644 index 128093b278..0000000000 --- a/test/EFCore.PG.FunctionalTests/NpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class NpgsqlFixture : ServiceProviderFixtureBase -{ - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs deleted file mode 100644 index 4c17028d63..0000000000 --- a/test/EFCore.PG.FunctionalTests/NpgsqlServiceCollectionExtensionsTest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class NpgsqlServiceCollectionExtensionsTest() : RelationalServiceCollectionExtensionsTestBase(NpgsqlTestHelpers.Instance); diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs deleted file mode 100644 index 9ce402dea0..0000000000 --- a/test/EFCore.PG.FunctionalTests/NpgsqlValueGenerationScenariosTest.cs +++ /dev/null @@ -1,633 +0,0 @@ -using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; - -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -public class NpgsqlValueGenerationScenariosTest -{ - private static readonly string DatabaseName = "NpgsqlValueGenerationScenariosTest"; - - [Fact] - public async Task Insert_with_sequence_id() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextSequence(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextSequence(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, blogs[0].Id); - Assert.Equal(2, blogs[1].Id); - } - } - - public class BlogContextSequence(string databaseName) : ContextBase(databaseName); - - [Fact] - public async Task Insert_with_sequence_HiLo() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextHiLo(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextHiLo(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(1, blogs[0].Id); - Assert.Equal(2, blogs[1].Id); - } - } - - public class BlogContextHiLo(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.UseHiLo(); - } - } - - [Fact] - public async Task Insert_with_default_value_from_sequence() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextDefaultValue(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - } - - using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name)) - { - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - Assert.Equal(2, blogs[2].Id); - Assert.Equal(3, blogs[3].Id); - } - } - - public class BlogContextDefaultValue(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .HasSequence("MySequence") - .HasMin(0) - .StartsAt(0); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValueSql("nextval('\"MySequence\"')"); - } - } - - public class BlogContextDefaultValueNoMigrations(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValue(); - } - } - - public class BlogWithStringKey - { - public string Id { get; set; } - public string Name { get; set; } - } - - [Fact] - public async Task Insert_with_key_default_value_from_sequence() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(77, blogs[0].Id); - Assert.Equal(78, blogs[1].Id); - } - } - - public class BlogContextKeyColumnWithDefaultValue(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .HasSequence("MySequence") - .StartsAt(77); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasDefaultValueSql("nextval('\"MySequence\"')") - .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); - } - } - - [ConditionalFact] - public async Task Insert_uint_to_Identity_column_using_value_converter() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithUIntKey { Name = "One Unicorn" }, new BlogWithUIntKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name)) - { - var blogs = context.UnsignedBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal((uint)1, blogs[0].Id); - Assert.Equal((uint)2, blogs[1].Id); - } - } - - public class BlogContextUIntToIdentityUsingValueConverter(string databaseName) : ContextBase(databaseName) - { - public DbSet UnsignedBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .HasConversion(); - } - } - - public class BlogWithUIntKey - { - public uint Id { get; set; } - public string Name { get; set; } - } - - [ConditionalFact] - public async Task Insert_string_to_Identity_column_using_value_converter() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) - { - context.Database.EnsureCreatedResiliently(); - - context.AddRange( - new BlogWithStringKey { Name = "One Unicorn" }, new BlogWithStringKey { Name = "Two Unicorns" }); - - context.SaveChanges(); - } - - using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name)) - { - var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal("1", blogs[0].Id); - Assert.Equal("2", blogs[1].Id); - } - } - - public class BlogContextStringToIdentityUsingValueConverter(string databaseName) : ContextBase(databaseName) - { - public DbSet StringyBlogs { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - Guid guid; - modelBuilder - .Entity() - .Property(e => e.Id) - .HasValueGenerator() - .HasConversion( - v => Guid.TryParse(v, out guid) - ? default - : int.Parse(v), - v => v.ToString()) - .ValueGeneratedOnAdd(); - } - } - - [Fact] - public async Task Insert_with_explicit_non_default_keys() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextNoKeyGeneration(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new Blog { Id = 66, Name = "One Unicorn" }, - new Blog { Id = 67, Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextNoKeyGeneration(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(66, blogs[0].Id); - Assert.Equal(67, blogs[1].Id); - } - } - - public class BlogContextNoKeyGeneration(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - } - } - - [Fact] - public async Task Insert_with_explicit_with_default_keys() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new NullableKeyBlog { Id = 0, Name = "One Unicorn" }, - new NullableKeyBlog { Id = 1, Name = "Two Unicorns" }); - context.SaveChanges(); - } - - using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) - { - var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(0, blogs[0].Id); - Assert.Equal(1, blogs[1].Id); - } - } - - public class BlogContextNoKeyGenerationNullableKey(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedNever(); - } - } - - [Fact] - public async Task Insert_with_non_key_default_value() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) - { - context.Database.EnsureCreated(); - var blogs = new List - { - new() { Name = "One Unicorn" }, new() { Name = "Two Unicorns", CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0) } - }; - context.AddRange(blogs); - context.SaveChanges(); - - Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); - Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); - } - - using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); - - Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); - Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); - - blogs[0].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); - blogs[1].Name = "Zwo Unicorns"; - context.SaveChanges(); - } - - using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); - - Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); - Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[0].CreatedOn); - } - } - - public class BlogContextNonKeyDefaultValue(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity() - .Property(e => e.CreatedOn) - .ValueGeneratedOnAdd() - .HasDefaultValueSql("now()"); - } - } - - [Fact] - public async Task Insert_with_non_key_default_value_readonly() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) - { - context.Database.EnsureCreated(); - context.AddRange( - new Blog { Name = "One Unicorn" }, - new Blog { Name = "Two Unicorns" }); - context.SaveChanges(); - Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn); - } - - DateTime dateTime0; - - using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - dateTime0 = blogs[0].CreatedOn; - - Assert.NotEqual(new DateTime(), dateTime0); - Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); - - blogs[0].Name = "One Pegasus"; - blogs[1].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); - - context.SaveChanges(); - } - - using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) - { - var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); - - Assert.Equal(dateTime0, blogs[0].CreatedOn); - Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[1].CreatedOn); - } - } - - public class BlogContextNonKeyReadOnlyDefaultValue(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity() - .Property(e => e.CreatedOn) - .HasDefaultValueSql("now()") - .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); - } - } - - [Fact] - public async Task Insert_with_serial_non_id() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - int afterSave; - - using (var context = new BlogContextSequenceNonId(testStore.Name)) - { - context.Database.EnsureCreated(); - - var blog = context.Add(new Blog { Name = "One Unicorn" }).Entity; - var beforeSave = blog.OtherId; - context.SaveChanges(); - afterSave = blog.OtherId; - - Assert.NotEqual(beforeSave, afterSave); - } - - using (var context = new BlogContextSequenceNonId(testStore.Name)) - { - Assert.Equal(afterSave, context.Blogs.Single().OtherId); - } - } - - public class BlogContextSequenceNonId(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder - .Entity() - .Property(e => e.OtherId) - .ValueGeneratedOnAdd(); - } - } - - [Fact] - public async Task Insert_with_client_generated_GUID_key() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - Guid afterSave; - using (var context = new BlogContext(testStore.Name)) - { - context.Database.EnsureCreated(); - var blog = context.Add(new GuidBlog { Name = "One Unicorn" }).Entity; - var beforeSave = blog.Id; - context.SaveChanges(); - afterSave = blog.Id; - - Assert.Equal(beforeSave, afterSave); - } - - using (var context = new BlogContext(testStore.Name)) - { - Assert.Equal(afterSave, context.GuidBlogs.Single().Id); - } - } - - public class BlogContext(string databaseName) : ContextBase(databaseName); - - [Fact] - public async Task Insert_with_server_generated_GUID_key() - { - await using var testStore = await NpgsqlTestStore.CreateInitializedAsync(DatabaseName); - - Guid afterSave; - using (var context = new BlogContextServerGuidKey(testStore.Name)) - { - context.Database.EnsureCreated(); - - var blog = context.Add( - new GuidBlog { Name = "One Unicorn" }).Entity; - var beforeSave = blog.Id; - var beforeSaveNotId = blog.NotId; - - Assert.Equal(default, beforeSave); - Assert.Equal(default, beforeSaveNotId); - - context.SaveChanges(); - - afterSave = blog.Id; - var afterSaveNotId = blog.NotId; - - Assert.NotEqual(default, afterSave); - Assert.NotEqual(default, afterSaveNotId); - Assert.NotEqual(beforeSave, afterSave); - Assert.NotEqual(beforeSaveNotId, afterSaveNotId); - } - - using (var context = new BlogContextServerGuidKey(testStore.Name)) - { - Assert.Equal(afterSave, context.GuidBlogs.Single().Id); - } - } - - public class BlogContextServerGuidKey(string databaseName) : ContextBase(databaseName) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.HasPostgresExtension("uuid-ossp"); - modelBuilder - .Entity( - eb => - { - eb.Property(e => e.Id) - .HasDefaultValueSql("uuid_generate_v4()"); - eb.Property(e => e.NotId) - .HasDefaultValueSql("uuid_generate_v4()"); - }); - } - } - - public class Blog - { - public int Id { get; set; } - public int OtherId { get; set; } - public string Name { get; set; } - public DateTime CreatedOn { get; set; } - } - - public class NullableKeyBlog - { - public int? Id { get; set; } - public string Name { get; set; } - public DateTime CreatedOn { get; set; } - } - - public class FullNameBlog - { - public int Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string FullName { get; set; } - } - - public class GuidBlog - { - public Guid Id { get; set; } - public string Name { get; set; } - public Guid NotId { get; set; } - } - - public class ConcurrentBlog - { - public int Id { get; set; } - public string Name { get; set; } - public byte[] Timestamp { get; set; } - } - - public abstract class ContextBase(string databaseName) : DbContext - { - public DbSet Blogs { get; set; } - public DbSet NullableKeyBlogs { get; set; } - public DbSet FullNameBlogs { get; set; } - public DbSet GuidBlogs { get; set; } - public DbSet ConcurrentBlogs { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .EnableServiceProviderCaching(false) - .UseNpgsql( - NpgsqlTestStore.CreateConnectionString(databaseName), - b => b.ApplyConfiguration()); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(b => b.CreatedOn).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(b => b.CreatedOn).HasColumnType("timestamp without time zone"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs deleted file mode 100644 index abadf6067b..0000000000 --- a/test/EFCore.PG.FunctionalTests/OptimisticConcurrencyNpgsqlTest.cs +++ /dev/null @@ -1,146 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; - -namespace Microsoft.EntityFrameworkCore; - -#nullable disable - -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(F1NpgsqlFixture fixture) : OptimisticConcurrencyNpgsqlTestBase(fixture); - -public abstract class OptimisticConcurrencyNpgsqlTestBase(TFixture fixture) - : OptimisticConcurrencyRelationalTestBase(fixture) - where TFixture : F1RelationalFixture, new() -{ - [ConditionalFact] - public async Task Modifying_concurrency_token_only_is_noop() - { - await using var c = CreateF1Context(); - await c.Database.CreateExecutionStrategy().ExecuteAsync( - c, async context => - { - await using var transaction = context.Database.BeginTransaction(); - var driver = context.Drivers.Single(d => d.CarNumber == 1); - driver.Podiums = StorePodiums; - var firstVersion = context.Entry(driver).Property("Version").CurrentValue; - await context.SaveChangesAsync(); - - await using var innerContext = CreateF1Context(); - innerContext.Database.UseTransaction(transaction.GetDbTransaction()); - driver = innerContext.Drivers.Single(d => d.CarNumber == 1); - Assert.NotEqual(firstVersion, innerContext.Entry(driver).Property("Version").CurrentValue); - Assert.Equal(StorePodiums, driver.Podiums); - - var secondVersion = innerContext.Entry(driver).Property("Version").CurrentValue; - innerContext.Entry(driver).Property("Version").CurrentValue = firstVersion; - await innerContext.SaveChangesAsync(); - await using var validationContext = CreateF1Context(); - validationContext.Database.UseTransaction(transaction.GetDbTransaction()); - driver = validationContext.Drivers.Single(d => d.CarNumber == 1); - Assert.Equal(secondVersion, validationContext.Entry(driver).Property("Version").CurrentValue); - Assert.Equal(StorePodiums, driver.Podiums); - }); - } - - [ConditionalFact] - public async Task Database_concurrency_token_value_is_updated_for_all_sharing_entities() - { - await using var c = CreateF1Context(); - await c.Database.CreateExecutionStrategy().ExecuteAsync( - c, async context => - { - await using var transaction = context.Database.BeginTransaction(); - var sponsor = context.Set().Single(); - var sponsorEntry = c.Entry(sponsor); - var detailsEntry = sponsorEntry.Reference(s => s.Details).TargetEntry; - var sponsorVersion = sponsorEntry.Property("Version").CurrentValue; - var detailsVersion = detailsEntry.Property("Version").CurrentValue; - - Assert.Null(sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); - sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue = 1; - - sponsor.Name = "Telecom"; - - Assert.Equal(sponsorVersion, detailsVersion); - - await context.SaveChangesAsync(); - - var newSponsorVersion = sponsorEntry.Property("Version").CurrentValue; - var newDetailsVersion = detailsEntry.Property("Version").CurrentValue; - - Assert.Equal(newSponsorVersion, newDetailsVersion); - Assert.NotEqual(sponsorVersion, newSponsorVersion); - - Assert.Equal(1, sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); - Assert.Equal(1, detailsEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); - }); - } - - [ConditionalFact] - public async Task Original_concurrency_token_value_is_used_when_replacing_owned_instance() - { - await using var c = CreateF1Context(); - await c.Database.CreateExecutionStrategy().ExecuteAsync( - c, async context => - { - await using var transaction = context.Database.BeginTransaction(); - var sponsor = context.Set().Single(); - var sponsorEntry = c.Entry(sponsor); - var sponsorVersion = sponsorEntry.Property("Version").CurrentValue; - - Assert.Null(sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); - sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue = 1; - - sponsor.Details = new SponsorDetails { Days = 11, Space = 51m }; - - context.ChangeTracker.DetectChanges(); - - var detailsEntry = sponsorEntry.Reference(s => s.Details).TargetEntry; - detailsEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue = 1; - - await context.SaveChangesAsync(); - - var newSponsorVersion = sponsorEntry.Property("Version").CurrentValue; - var newDetailsVersion = detailsEntry.Property("Version").CurrentValue; - - Assert.Equal(newSponsorVersion, newDetailsVersion); - Assert.NotEqual(sponsorVersion, newSponsorVersion); - - Assert.Equal(1, sponsorEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); - Assert.Equal(1, detailsEntry.Property(Sponsor.ClientTokenPropertyName).CurrentValue); - }); - } - - public override void Property_entry_original_value_is_set() - { - base.Property_entry_original_value_is_set(); - - AssertSql( - """ -SELECT e."Id", e."EngineSupplierId", e."Name", e."StorageLocation_Latitude", e."StorageLocation_Longitude" -FROM "Engines" AS e -ORDER BY e."Id" NULLS FIRST -LIMIT 1 -""", - // - """ -@p1='1' -@p2='Mercedes' -@p0='FO 108X' -@p3='ChangedEngine' -@p4='47.64491' (Nullable = true) -@p5='-122.128101' (Nullable = true) - -UPDATE "Engines" SET "Name" = @p0 -WHERE "Id" = @p1 AND "EngineSupplierId" = @p2 AND "Name" = @p3 AND "StorageLocation_Latitude" = @p4 AND "StorageLocation_Longitude" = @p5; -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); -} diff --git a/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs deleted file mode 100644 index ec0c233d60..0000000000 --- a/test/EFCore.PG.FunctionalTests/OverzealousInitializationNpgsqlTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class OverzealousInitializationNpgsqlTest(OverzealousInitializationNpgsqlTest.OverzealousInitializationNpgsqlFixture fixture) - : OverzealousInitializationTestBase(fixture) -{ - public class OverzealousInitializationNpgsqlFixture : OverzealousInitializationFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs deleted file mode 100644 index cdbf39685d..0000000000 --- a/test/EFCore.PG.FunctionalTests/PropertyValuesNpgsqlTest.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class PropertyValuesNpgsqlTest(PropertyValuesNpgsqlTest.PropertyValuesNpgsqlFixture fixture) - : PropertyValuesRelationalTestBase(fixture) -{ - public class PropertyValuesNpgsqlFixture : PropertyValuesRelationalFixture - { - protected override string StoreName { get; } = "PropertyValues"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(e => e.TerminationDate).HasColumnType("timestamp without time zone"); - - modelBuilder.Entity() - .Property(b => b.Value).HasColumnType("decimal(18,2)"); - - modelBuilder.Entity() - .Property(ce => ce.LeaveBalance).HasColumnType("decimal(18,2)"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs deleted file mode 100644 index 6d9c52e36d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocAdvancedMappingsQueryNpgsqlTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocAdvancedMappingsQueryNpgsqlTest(NonSharedFixture fixture) - : AdHocAdvancedMappingsQueryRelationalTestBase(fixture) -{ - 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 deleted file mode 100644 index 83cc8d30e2..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocComplexTypeQueryNpgsqlTest.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocComplexTypeQueryNpgsqlTest(NonSharedFixture fixture) : AdHocComplexTypeQueryTestBase(fixture) -{ - // Test is SQL Server-specific and being removed, https://github.com/dotnet/efcore/pull/37177 - public override Task Complex_type_equality_with_non_default_type_mapping() - => Task.CompletedTask; - - 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_Id='1' (Nullable = true) -@entity_equality_container_Containee1_Id='2' (Nullable = true) -@entity_equality_container_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_Id AND e."ComplexContainer_Containee1_Id" = @entity_equality_container_Containee1_Id AND e."ComplexContainer_Containee2_Id" = @entity_equality_container_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 deleted file mode 100644 index e613d913cf..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocJsonQueryNpgsqlTest.cs +++ /dev/null @@ -1,317 +0,0 @@ -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class AdHocJsonQueryNpgsqlTest(NonSharedFixture fixture) : AdHocJsonQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override async Task Seed29219(DbContext ctx) - { - var entity1 = new Context29219.MyEntity - { - Id = 1, - Reference = new Context29219.MyJsonEntity { NonNullableScalar = 10, NullableScalar = 11 }, - Collection = - [ - new() { NonNullableScalar = 100, NullableScalar = 101 }, - new() { NonNullableScalar = 200, NullableScalar = 201 }, - new() { NonNullableScalar = 300, NullableScalar = null } - ] - }; - - var entity2 = new Context29219.MyEntity - { - Id = 2, - Reference = new Context29219.MyJsonEntity { 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 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) -"""); - } - - #region BadJsonProperties - - // PostgreSQL stores JSON as jsonb, which doesn't allow badly-formed JSON; so the following tests are irrelevant. - - public override async Task Bad_json_properties_duplicated_navigations(bool noTracking) - { - if (noTracking) - { - await Assert.ThrowsAsync(() => base.Bad_json_properties_duplicated_navigations(noTracking: true)); - } - else - { - await base.Bad_json_properties_duplicated_navigations(noTracking: false); - } - } - - public override Task Bad_json_properties_duplicated_scalars(bool noTracking) - => Assert.ThrowsAsync(() => base.Bad_json_properties_duplicated_scalars(noTracking)); - - public override Task Bad_json_properties_empty_navigations(bool noTracking) - => Assert.ThrowsAsync(() => base.Bad_json_properties_empty_navigations(noTracking)); - - public override Task Bad_json_properties_empty_scalars(bool noTracking) - => Assert.ThrowsAsync(() => base.Bad_json_properties_empty_scalars(noTracking)); - - public override Task Bad_json_properties_null_navigations(bool noTracking) - => Assert.ThrowsAsync(() => base.Bad_json_properties_null_navigations(noTracking)); - - public override Task Bad_json_properties_null_scalars(bool noTracking) - => Assert.ThrowsAsync(() => base.Bad_json_properties_null_scalars(noTracking)); - - protected override Task SeedBadJsonProperties(ContextBadJsonProperties ctx) - => throw new NotSupportedException("PostgreSQL stores JSON as jsonb, which doesn't allow badly-formed JSON"); - - #endregion - - [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; } - } - - #region Problematc tests (Unspecified DateTime) - - // These tests use a model with a non-UTC DateTime, which isn't supported in PG's timestamp with time zone - - public override Task Project_entity_with_json_null_values() - => Assert.ThrowsAsync(() => base.Project_entity_with_json_null_values()); - - public override Task Try_project_collection_but_JSON_is_entity() - => Assert.ThrowsAsync(() => base.Try_project_collection_but_JSON_is_entity()); - - public override Task Try_project_reference_but_JSON_is_collection() - => Assert.ThrowsAsync(() => base.Try_project_reference_but_JSON_is_collection()); - - public override Task Project_entity_with_optional_json_entity_owned_by_required_json() - => Assert.ThrowsAsync(() => base.Project_entity_with_optional_json_entity_owned_by_required_json()); - - public override Task Project_required_json_entity() - => Assert.ThrowsAsync(() => base.Project_required_json_entity()); - - public override Task Project_optional_json_entity_owned_by_required_json_entity() - => Assert.ThrowsAsync(() => base.Project_optional_json_entity_owned_by_required_json_entity()); - - public override Task Project_missing_required_scalar(bool async) - => Assert.ThrowsAsync(() => base.Project_missing_required_scalar(async)); - - public override Task Project_nested_json_entity_with_missing_scalars(bool async) - => Assert.ThrowsAsync(() => base.Project_nested_json_entity_with_missing_scalars(async)); - - public override Task Project_null_required_scalar(bool async) - => Assert.ThrowsAsync(() => base.Project_null_required_scalar(async)); - - public override Task Project_root_entity_with_missing_required_navigation(bool async) - => Assert.ThrowsAsync(() => base.Project_root_entity_with_missing_required_navigation(async)); - - public override Task Project_root_entity_with_null_required_navigation(bool async) - => Assert.ThrowsAsync(() => base.Project_root_entity_with_null_required_navigation(async)); - - public override Task Project_root_with_missing_scalars(bool async) - => Assert.ThrowsAsync(() => base.Project_root_with_missing_scalars(async)); - - public override Task Project_top_level_json_entity_with_missing_scalars(bool async) - => Assert.ThrowsAsync(() => base.Project_top_level_json_entity_with_missing_scalars(async)); - - public override Task Project_missing_required_navigation(bool async) - => Task.CompletedTask; // Different exception expected in the base implementation - - public override Task Project_null_required_navigation(bool async) - => Task.CompletedTask; // Different exception expected in the base implementation - - public override Task Project_top_level_entity_with_null_value_required_scalars(bool async) - => Task.CompletedTask; // Different exception expected in the base implementation - - #endregion Problematc tests (Unspecified DateTime) - - protected void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocManyToManyQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocManyToManyQueryNpgsqlTest.cs deleted file mode 100644 index c06db71b45..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocManyToManyQueryNpgsqlTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocManyToManyQueryNpgsqlTest(NonSharedFixture fixture) : AdHocManyToManyQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs deleted file mode 100644 index aa9f5700e9..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocMiscellaneousQueryNpgsqlTest(NonSharedFixture fixture) : AdHocMiscellaneousQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode) - { - new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); - - return optionsBuilder; - } - - // Unlike the other providers, EFCore.PG does actually support mapping JsonElement - public override Task Mapping_JsonElement_property_throws_a_meaningful_exception() - => Task.CompletedTask; - - 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; - - // Writes DateTime with Kind=Unspecified to timestamptz - public override Task Subquery_first_member_compared_to_null(bool async) - => Task.CompletedTask; - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/27995/files#r874038747")] - public override Task StoreType_for_UDF_used(bool async) - => base.StoreType_for_UDF_used(async); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocNavigationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocNavigationsQueryNpgsqlTest.cs deleted file mode 100644 index 023bd30eca..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocNavigationsQueryNpgsqlTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocNavigationsQueryNpgsqlTest(NonSharedFixture fixture) : AdHocNavigationsQueryRelationalTestBase(fixture) -{ - // 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 deleted file mode 100644 index f593297f48..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocPrecompiledQueryNpgsqlTest.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocPrecompiledQueryNpgsqlTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) - : AdHocPrecompiledQueryRelationalTestBase(fixture, 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='1' - -SELECT j."Id", j."IntList", j."JsonThing" -FROM "JsonEntities" AS j -WHERE j."IntList"[@id + 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/AdHocQueryFiltersQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocQueryFiltersQueryNpgsqlTest.cs deleted file mode 100644 index ac65f0f1f8..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocQueryFiltersQueryNpgsqlTest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocQueryFiltersQueryNpgsqlTest(NonSharedFixture fixture) - : AdHocQueryFiltersQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocQuerySplittingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocQuerySplittingQueryNpgsqlTest.cs deleted file mode 100644 index 49d04a010c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocQuerySplittingQueryNpgsqlTest.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class AdHocQuerySplittingQueryNpgsqlTest(NonSharedFixture fixture) : AdHocQuerySplittingQueryTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - private static readonly FieldInfo _querySplittingBehaviorFieldInfo = - typeof(RelationalOptionsExtension).GetField("_querySplittingBehavior", BindingFlags.NonPublic | BindingFlags.Instance); - - 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; - } - - protected override TestStore CreateTestStore25225() - { - var testStore = NpgsqlTestStore.Create(StoreName); - testStore.UseConnectionString = true; - return testStore; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs deleted file mode 100644 index 083d4fa224..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs +++ /dev/null @@ -1,989 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Array; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; - -// ReSharper disable ConvertToConstant.Local - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ArrayArrayQueryTest : QueryTestBase -{ - public ArrayArrayQueryTest(ArrayArrayQueryFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Roundtrip - - [ConditionalFact] - public virtual void Roundtrip() - { - using var ctx = CreateContext(); - var x = ctx.SomeEntities.Single(e => e.Id == 1); - - Assert.Equal([3, 4], x.IntArray); - Assert.Equal([3, 4], x.IntList); - Assert.Equal([3, 4, null], x.NullableIntArray); - Assert.Equal( - [ - 3, - 4, - null - ], x.NullableIntList); - } - - #endregion - - #region Indexers - - [ConditionalFact] - public virtual async Task Index_with_constant() - { await AssertQuery(ss => ss.Set().Where(e => e.IntArray[0] == 3)); - - 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 s."IntArray"[1] = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Index_with_parameter() - { - // ReSharper disable once ConvertToConstant.Local - var x = 0; - await AssertQuery(ss => ss.Set().Where(e => e.IntArray[x] == 3)); - - AssertSql( - """ -@x='0' - -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 + 1] = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_index_with_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntArray[0] == 3)); - - 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 s."NullableIntArray"[1] = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_value_array_index_compare_to_null() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntArray[2] == null)); - - 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 s."NullableIntArray"[3] IS NULL -"""); - } - - [ConditionalFact] - public virtual async Task Non_nullable_value_array_index_compare_to_null() - { -#pragma warning disable CS0472 - await AssertQuery( - ss => ss.Set().Where(e => e.IntArray[1] == null), - assertEmpty: true); -#pragma warning restore CS0472 - - 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 FALSE -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_reference_array_index_compare_to_null() - { - await AssertQuery(ss => ss.Set() - .Where(e => e.NullableStringArray[2] == null)); - - 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 s."NullableStringArray"[3] IS NULL -"""); - } - - [ConditionalFact] - public virtual async Task Non_nullable_reference_array_index_compare_to_null() - { -#pragma warning disable CS0472 - await AssertQuery(ss => ss.Set().Where(e => e.StringArray[1] == null), - assertEmpty: true); -#pragma warning restore CS0472 - - 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 FALSE -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Index_bytea_with_constant(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(e => e.Bytea[0] == 3)); - - 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 get_byte(s."Bytea", 0) = 3 -"""); - } - - #endregion - - #region SequenceEqual - - [ConditionalFact] - public virtual async Task SequenceEqual_with_parameter() - { - var arr = new[] { 3, 4 }; - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.SequenceEqual(arr))); - - AssertSql( - """ -@arr={ '3' -'4' } (DbType = Object) - -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 -"""); - } - -// [ConditionalFact] -// public virtual async Task SequenceEqual_with_array_literal() -// { -// await AssertQuery(ss => ss.Set().Where(e => e.IntArray.SequenceEqual(new[] { 3, 4 }))); - -// AssertSql( -// """ -// SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" -// FROM "SomeEntities" AS s -// WHERE s."IntArray" = ARRAY[3,4]::integer[] -// """); -// } - -// [ConditionalFact] -// public virtual async Task SequenceEqual_over_nullable_with_parameter() -// { -// var arr = new int?[] { 3, 4, null }; -// await AssertQuery(ss => ss.Set().Where(e => e.NullableIntArray.SequenceEqual(arr))); -// AssertSql( -// """ -// @arr={ '3', '4', NULL } (DbType = Object) - -// 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 -// """); -// } - - #endregion SequenceEqual - - #region Containment - - [ConditionalFact] - public virtual async Task Array_column_Any_equality_operator() - { - await AssertQuery(ss => ss.Set().Where(e => e.StringArray.Any(p => p == "3"))); - - 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 '3' = ANY (s."StringArray") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Any_Equals() - { - await AssertQuery(ss => ss.Set().Where(e => e.StringArray.Any(p => "3".Equals(p)))); - - 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 '3' = ANY (s."StringArray") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_literal_item() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Contains(3))); - - 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 3 = ANY (s."IntArray") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_parameter_item() - { - var p = 3; - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Contains(p))); - - AssertSql( - """ -@p='3' - -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 @p = ANY (s."IntArray") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_column_item() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Contains(e.Id + 2))); - - 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 s."Id" + 2 = ANY (s."IntArray") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_null_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableStringArray.Contains(null))); - - 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_position(s."NullableStringArray", NULL) IS NOT NULL -"""); - } - - [ConditionalFact] - public virtual void Array_column_Contains_null_parameter_does_not_work() - { - using var ctx = CreateContext(); - - string? p = null; - - // We incorrectly miss arrays containing non-constant nulls, because detecting those - // would prevent index use. - Assert.Equal( - 0, - ctx.SomeEntities.Count(e => e.StringArray.Contains(p))); - - AssertSql( - """ -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE NULL = ANY (s."StringArray") OR (NULL IS NULL AND array_position(s."StringArray", NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_array_column_Contains_literal_item() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntArray.Contains(3))); - - 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 3 = ANY (s."NullableIntArray") -"""); - } - - [ConditionalFact] - public virtual async Task Array_constant_Contains_column() - { - await AssertQuery(ss => ss.Set().Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText))); - - 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 s."NullableText" IN ('foo', 'xxx') -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_nullable_column() - { - var array = new[] { "foo", "xxx" }; - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.NullableText))); - - AssertSql( - """ -@array={ 'foo' -'xxx' } (DbType = Object) - -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) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_non_nullable_column() - { - var array = new[] { 1 }; - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.Id))); - - AssertSql( - """ -@array={ '1' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual void Array_param_with_null_Contains_non_nullable_not_found() - { - using var ctx = CreateContext(); - - var array = new[] { "unknown1", "unknown2", null }; - - Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE s."NonNullableText" = ANY (@array) -"""); - } - - [ConditionalFact] - public virtual void Array_param_with_null_Contains_non_nullable_not_found_negated() - { - using var ctx = CreateContext(); - - var array = new[] { "unknown1", "unknown2", null }; - - Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE NOT (s."NonNullableText" = ANY (@array) AND s."NonNullableText" = ANY (@array) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual void Array_param_with_null_Contains_nullable_not_found() - { - using var ctx = CreateContext(); - - var array = new[] { "unknown1", "unknown2", null }; - - Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE s."NullableText" = ANY (@array) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual void Array_param_with_null_Contains_nullable_not_found_negated() - { - using var ctx = CreateContext(); - - var array = new[] { "unknown1", "unknown2", null }; - - Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE NOT (s."NullableText" = ANY (@array) AND s."NullableText" = ANY (@array) IS NOT NULL) AND (s."NullableText" IS NOT NULL OR array_position(@array, NULL) IS NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_column_with_ToString() - { - var values = new[] { "1", "999" }; - await AssertQuery(ss => ss.Set().Where(e => values.Contains(e.Id.ToString()))); - - AssertSql( - """ -@values={ '1' -'999' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Byte_array_parameter_contains_column() - { - var values = new byte[] { 20 }; - - await AssertQuery( - ss => ss.Set().Where(e => values.Contains(e.Byte))); - - // Note: EF Core prints the parameter as a bytea, but it's actually a smallint[] (otherwise ANY would fail) - AssertSql( - """ -@values='0x14' (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_enum_to_int() - { - var array = new[] { SomeEnum.Two, SomeEnum.Three }; - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToInt))); - - AssertSql( - """ -@array={ '-2' -'-3' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_enum_to_string() - { - var array = new[] { SomeEnum.Two, SomeEnum.Three }; - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToString))); - - AssertSql( - """ -@array={ 'Two' -'Three' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_nullable_enum_to_string() - { - var array = new SomeEnum?[] { SomeEnum.Two, SomeEnum.Three }; - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToString))); - - AssertSql( - """ -@array={ 'Two' -'Three' } (DbType = Object) - -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) OR (s."NullableEnumConvertedToString" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_nullable_enum_to_string_with_non_nullable_lambda() - { - var array = new SomeEnum?[] { SomeEnum.Two, SomeEnum.Three }; - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToStringWithNonNullableLambda))); - - AssertSql( - """ -@array={ 'Two' -'Three' } (DbType = Object) - -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) OR (s."NullableEnumConvertedToStringWithNonNullableLambda" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_value_converted_param() - { - var item = SomeEnum.Eight; - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Contains(item))); - - AssertSql( - """ -@item='Eight' (Nullable = false) - -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 @item = ANY (s."ValueConvertedArrayOfEnum") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_value_converted_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Contains(SomeEnum.Eight))); - - 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 'Eight' = ANY (s."ValueConvertedArrayOfEnum") -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_array_column() - { - var p = new[] { SomeEnum.Eight, SomeEnum.Nine }; - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.All(x => p.Contains(x)))); - - AssertSql( - """ -@p={ 'Eight' -'Nine' } (DbType = Object) - -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."ValueConvertedArrayOfEnum" <@ @p -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_in_scalar_subquery() - { - await AssertQuery( - ss => ss.Set().Where(c => c.ArrayEntities.OrderBy(e => e.Id).First().NullableIntArray.Contains(3))); - - AssertSql( - """ -SELECT s."Id" -FROM "SomeEntityContainers" AS s -WHERE 3 = ANY (( - SELECT s0."NullableIntArray" - FROM "SomeEntities" AS s0 - WHERE s."Id" = s0."ArrayContainerEntityId" - ORDER BY s0."Id" NULLS FIRST - LIMIT 1)::integer[]) -"""); - } - - [ConditionalFact] - public virtual async Task IList_column_contains_constant() - { - await AssertQuery(ss => ss.Set().Where(a => a.IList.Contains(10))); - - 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 10 = ANY (s."IList") -"""); - } - - #endregion Containment - - #region Length/Count - - [ConditionalFact] - public virtual async Task Array_Length() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Length == 2)); - - 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 cardinality(s."IntArray") = 2 -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_array_Length() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntArray.Length == 3)); - - 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 cardinality(s."NullableIntArray") = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Array_Length_on_EF_Property() - { - await AssertQuery( - ss => ss.Set().Where(e => EF.Property(e, nameof(ArrayEntity.IntArray)).Length == 2)); - - 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 cardinality(s."IntArray") = 2 -"""); - } - - #endregion Length/Count - - #region Any/All - - [ConditionalFact] - public virtual async Task Any_no_predicate() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Any())); - - 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 cardinality(s."IntArray") > 0 -"""); - } - - [ConditionalFact] - public virtual async Task Any_like() - { - await AssertQuery( - ss => ss.Set() - .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))), ss => ss.Set() - .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); - - 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 s."NullableText" LIKE ANY (ARRAY['a%','b%','c%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task Any_ilike() - { - await AssertQuery( - ss => ss.Set() - .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText!, p))), ss => ss.Set() - .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); - - 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 s."NullableText" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task Any_like_anonymous() - { - await using var ctx = CreateContext(); - - var patternsActual = new[] { "a%", "b%", "c%" }; - var patternsExpected = new[] { "a", "b", "c" }; - - await AssertQuery( - ss => ss.Set() - .Where(e => patternsActual.Any(p => EF.Functions.Like(e.NullableText, p))), ss => ss.Set() - .Where(e => patternsExpected.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); - - AssertSql( - """ -@patternsActual={ 'a%' -'b%' -'c%' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task All_like() - { - await AssertQuery( - ss => ss.Set() - .Where(e => new[] { "b%", "ba%" }.All(p => EF.Functions.Like(e.NullableText, p))), ss => ss.Set() - .Where(e => new[] { "b", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); - - 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 s."NullableText" LIKE ALL (ARRAY['b%','ba%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task All_ilike() - { - await AssertQuery( - ss => ss.Set() - .Where(e => new[] { "B%", "ba%" }.All(p => EF.Functions.ILike(e.NullableText!, p))), ss => ss.Set() - .Where(e => new[] { "B", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); - - 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 s."NullableText" ILIKE ALL (ARRAY['B%','ba%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_on_constant_array() - { - await AssertQuery(ss => ss.Set().Where(e => new[] { 2, 3 }.Any(p => e.IntArray.Contains(p)))); - - 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[2,3]::integer[] && s."IntArray" -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_between_column_and_List() - { - var ints = new List { 2, 3 }; - - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Any(i => ints.Contains(i)))); - - AssertSql( - """ -@ints={ '2' -'3' } (DbType = Object) - -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 -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_between_column_and_array() - { - var ints = new[] { 2, 3 }; - - await AssertQuery(ss => ss.Set().Where(e => e.IntArray.Any(i => ints.Contains(i)))); - - AssertSql( - """ -@ints={ '2' -'3' } (DbType = Object) - -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 -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_between_column_and_other_type() - { - var list = new List { SomeEnum.Eight }; - - await AssertQuery( - ss => ss.Set().Where(e => e.ValueConvertedArrayOfEnum.Any(i => list.Contains(i)))); - - AssertSql( - """ -@list={ 'Eight' } (DbType = Object) - -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."ValueConvertedArrayOfEnum" && @list -"""); - } - - [ConditionalFact] - public virtual async Task All_Contains() - { - await AssertQuery(ss => ss.Set().Where(e => new[] { 5, 6 }.All(p => e.IntArray.Contains(p)))); - - 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[5,6]::integer[] <@ s."IntArray" -"""); - } - - #endregion Any/All - - #region Other translations - - [ConditionalFact] - public virtual async Task Append() - // TODO: https://github.com/dotnet/efcore/issues/30669 - => await AssertTranslationFailed(() => AssertQuery( - ss => ss.Set() - .Where(e => e.IntArray.Append(5).SequenceEqual(new[] { 3, 4, 5 })))); - - // await base.Append(async); - // - // AssertSql( - // """ - // SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" - // FROM "SomeEntities" AS s - // WHERE array_append(s."IntArray", 5) = ARRAY[3,4,5]::integer[] - // """); - - // [ConditionalFact] - // public virtual async Task Concat() - // { - // await AssertQuery( - // ss => ss.Set() - // .Where(e => e.IntArray.Concat(new[] { 5, 6 }).SequenceEqual(new[] { 3, 4, 5, 6 }))); - -// AssertSql( -// """ -// SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" -// FROM "SomeEntities" AS s -// WHERE array_cat(s."IntArray", ARRAY[5,6]::integer[]) = ARRAY[3,4,5,6]::integer[] -// """); -// } - - [ConditionalFact] - public virtual async Task Array_IndexOf1() - { - await AssertQuery( - ss => ss.Set().Where(e => Array.IndexOf(e.IntArray, 6) == 1)); - - 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 COALESCE(array_position(s."IntArray", 6) - 1, -1) = 1 -"""); - } - - [ConditionalFact] - public virtual async Task Array_IndexOf2() - { - await AssertQuery( - ss => ss.Set().Where(e => Array.IndexOf(e.IntArray, 6, 1) == 1)); - - 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 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 - [ConditionalFact] - public virtual async Task String_Join_with_array_of_int_column() - { - await AssertQuery( - ss => ss.Set() - .Where(e => string.Join(", ", e.IntArray) == "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."IntArray", ', ', '') = '3, 4' -"""); - } - - [ConditionalFact] - public virtual async Task String_Join_with_array_of_string_column() - { - // This is not in ArrayQueryTest because string.Join uses another overload for string[] than for List and thus - // ArrayToListReplacingExpressionVisitor won't work. - await AssertQuery( - 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 virtual 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 - - protected void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected ArrayQueryContext CreateContext() - => Fixture.CreateContext(); - - public class ArrayArrayQueryFixture : ArrayQueryFixture - { - protected override string StoreName - => "ArrayQueryTest"; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs deleted file mode 100644 index 12dadfd911..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs +++ /dev/null @@ -1,984 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Array; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ArrayListQueryTest : QueryTestBase -{ - public ArrayListQueryTest(ArrayListQueryFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Roundtrip - - [ConditionalFact] - public virtual void Roundtrip() - { - using var ctx = CreateContext(); - var x = ctx.SomeEntities.Single(e => e.Id == 1); - - Assert.Equal([3, 4], x.IntArray); - Assert.Equal([3, 4], x.IntList); - Assert.Equal([3, 4, null], x.NullableIntArray); - Assert.Equal( - [ - 3, - 4, - null - ], x.NullableIntList); - } - - #endregion - - #region Indexers - - [ConditionalFact] - public virtual async Task Index_with_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList[0] == 3)); - - 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 s."IntList"[1] = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Index_with_parameter() - { - var x = 0; - await AssertQuery(ss => ss.Set().Where(e => e.IntList[x] == 3)); - - AssertSql( - """ -@x='0' - -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 + 1] = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_index_with_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntList[0] == 3)); - - 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 s."NullableIntList"[1] = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_value_array_index_compare_to_null() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntList[2] == null)); - - 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 s."NullableIntList"[3] IS NULL -"""); - } - -#pragma warning disable CS0472 - [ConditionalFact] - public virtual async Task Non_nullable_value_array_index_compare_to_null() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList[1] == null), assertEmpty: true); - - 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 FALSE -"""); - } -#pragma warning restore CS0472 - - [ConditionalFact] - public virtual async Task Nullable_reference_array_index_compare_to_null() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableStringList[2] == null)); - - 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 s."NullableStringList"[3] IS NULL -"""); - } - - [ConditionalFact] - public virtual async Task Non_nullable_reference_array_index_compare_to_null() - { - await AssertQuery(ss => ss.Set().Where(e => e.StringList[1] == null), assertEmpty: true); - - 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 FALSE -"""); - } - - #endregion - - #region SequenceEqual - - [ConditionalFact] - public virtual async Task SequenceEqual_with_parameter() - { - var arr = new[] { 3, 4 }; - await AssertQuery(ss => ss.Set().Where(e => e.IntList.SequenceEqual(arr))); - - AssertSql( - """ -@arr={ '3' -'4' } (DbType = Object) - -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 -"""); - } - -// [ConditionalFact] -// public virtual async Task SequenceEqual_with_array_literal() -// { -// await AssertQuery(ss => ss.Set().Where(e => e.IntList.SequenceEqual(new[] { 3, 4 }))); - -// AssertSql( -// """ -// SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" -// FROM "SomeEntities" AS s -// WHERE s."IntList" = ARRAY[3,4]::integer[] -// """); -// } - - [ConditionalFact] - public virtual async Task SequenceEqual_over_nullable_with_parameter() - { - var arr = new int?[] { 3, 4, null }; - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntList.SequenceEqual(arr))); - - AssertSql( - """ -@arr={ '3' -'4' -NULL } (DbType = Object) - -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 -"""); - } - - #endregion SequenceEqual - - #region Containment - - [ConditionalFact] - public virtual async Task Array_column_Any_equality_operator() - { - await AssertQuery(ss => ss.Set().Where(e => e.StringList.Any(p => p == "3"))); - - 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 '3' = ANY (s."StringList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Any_Equals() - { - await AssertQuery(ss => ss.Set().Where(e => e.StringList.Any(p => "3".Equals(p)))); - - 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 '3' = ANY (s."StringList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_literal_item() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Contains(3))); - - 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 3 = ANY (s."IntList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_parameter_item() - { - var p = 3; - - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Contains(p))); - - AssertSql( - """ -@p='3' - -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 @p = ANY (s."IntList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_column_item() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Contains(e.Id + 2))); - - 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 s."Id" + 2 = ANY (s."IntList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_null_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableStringList.Contains(null))); - - 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_position(s."NullableStringList", NULL) IS NOT NULL -"""); - } - - public void Array_column_Contains_null_parameter_does_not_work() - { - using var ctx = CreateContext(); - - string? p = null; - - // We incorrectly miss arrays containing non-constant nulls, because detecting those - // would prevent index use. - Assert.Equal( - 0, - ctx.SomeEntities.Count(e => e.StringList.Contains(p!))); - - AssertSql( - """ -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE NULL = ANY (s."StringList") OR (NULL IS NULL AND array_position(s."StringList", NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_array_column_Contains_literal_item() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntList.Contains(3))); - - 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 3 = ANY (s."NullableIntList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_constant_Contains_column() - { - await AssertQuery(ss => ss.Set().Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText))); - - 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 s."NullableText" IN ('foo', 'xxx') -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_nullable_column() - { - var array = new List { "foo", "xxx" }; - - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.NullableText!))); - - AssertSql( - """ -@array={ 'foo' -'xxx' } (DbType = Object) - -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) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_non_nullable_column() - { - var array = new List { 1 }; - - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.Id))); - - AssertSql( - """ -@array={ '1' } (DbType = Object) - -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) -"""); - } - - public void Array_param_with_null_Contains_non_nullable_not_found() - { - using var ctx = CreateContext(); - - var array = new List - { - "unknown1", - "unknown2", - null - }; - - Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE s."NonNullableText" = ANY (@array) -"""); - } - - public void Array_param_with_null_Contains_non_nullable_not_found_negated() - { - using var ctx = CreateContext(); - - var array = new List - { - "unknown1", - "unknown2", - null - }; - - Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE NOT (s."NonNullableText" = ANY (@array) AND s."NonNullableText" = ANY (@array) IS NOT NULL) -"""); - } - - public void Array_param_with_null_Contains_nullable_not_found() - { - using var ctx = CreateContext(); - - var array = new List - { - "unknown1", - "unknown2", - null - }; - - Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NullableText))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE s."NullableText" = ANY (@array) OR (s."NullableText" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - public void Array_param_with_null_Contains_nullable_not_found_negated() - { - using var ctx = CreateContext(); - - var array = new List - { - "unknown1", - "unknown2", - null - }; - - Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NullableText!))); - - AssertSql( - """ -@array={ 'unknown1' -'unknown2' -NULL } (DbType = Object) - -SELECT count(*)::int -FROM "SomeEntities" AS s -WHERE NOT (s."NullableText" = ANY (@array) AND s."NullableText" = ANY (@array) IS NOT NULL) AND (s."NullableText" IS NOT NULL OR array_position(@array, NULL) IS NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_column_with_ToString() - { - var values = new List { "1", "999" }; - - await AssertQuery(ss => ss.Set().Where(e => values.Contains(e.Id.ToString()))); - - AssertSql( - """ -@values={ '1' -'999' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Byte_array_parameter_contains_column() - { - var values = new List { 20 }; - - await AssertQuery(ss => ss.Set().Where(e => values.Contains(e.Byte))); - - AssertSql( - """ -@values={ '20' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_enum_to_int() - { - var array = new List { SomeEnum.Two, SomeEnum.Three }; - - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToInt))); - - AssertSql( - """ -@array={ '-2' -'-3' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_enum_to_string() - { - var array = new List { SomeEnum.Two, SomeEnum.Three }; - - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.EnumConvertedToString))); - - AssertSql( - """ -@array={ 'Two' -'Three' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_nullable_enum_to_string() - { - var array = new List { SomeEnum.Two, SomeEnum.Three }; - - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToString))); - - AssertSql( - """ -@array={ 'Two' -'Three' } (DbType = Object) - -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) OR (s."NullableEnumConvertedToString" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_column_nullable_enum_to_string_with_non_nullable_lambda() - { - var array = new List { SomeEnum.Two, SomeEnum.Three }; - - await AssertQuery(ss => ss.Set().Where(e => array.Contains(e.NullableEnumConvertedToStringWithNonNullableLambda))); - - AssertSql( - """ -@array={ 'Two' -'Three' } (DbType = Object) - -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) OR (s."NullableEnumConvertedToStringWithNonNullableLambda" IS NULL AND array_position(@array, NULL) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_value_converted_param() - { - var item = SomeEnum.Eight; - - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Contains(item))); - - AssertSql( - """ -@item='Eight' (Nullable = false) - -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 @item = ANY (s."ValueConvertedListOfEnum") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_value_converted_constant() - { - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Contains(SomeEnum.Eight))); - - 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 'Eight' = ANY (s."ValueConvertedListOfEnum") -"""); - } - - [ConditionalFact] - public virtual async Task Array_param_Contains_value_converted_array_column() - { - var p = new List { SomeEnum.Eight, SomeEnum.Nine }; - - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.All(x => p.Contains(x)))); - - AssertSql( - """ -@p={ 'Eight' -'Nine' } (DbType = Object) - -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."ValueConvertedListOfEnum" <@ @p -"""); - } - - [ConditionalFact] - public virtual async Task IList_column_contains_constant() - { - await AssertQuery(ss => ss.Set().Where(a => a.IList.Contains(10))); - - 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 10 = ANY (s."IList") -"""); - } - - [ConditionalFact] - public virtual async Task Array_column_Contains_in_scalar_subquery() - { - await AssertQuery(ss => ss.Set().Where(c => c.ArrayEntities.OrderBy(e => e.Id).First().NullableIntList.Contains(3))); - - AssertSql( - """ -SELECT s."Id" -FROM "SomeEntityContainers" AS s -WHERE 3 = ANY (( - SELECT s0."NullableIntList" - FROM "SomeEntities" AS s0 - WHERE s."Id" = s0."ArrayContainerEntityId" - ORDER BY s0."Id" NULLS FIRST - LIMIT 1)::integer[]) -"""); - } - - #endregion Containment - - #region Length/Count - - [ConditionalFact] - public virtual async Task Array_Length() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Count == 2)); - - 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 cardinality(s."IntList") = 2 -"""); - } - - [ConditionalFact] - public virtual async Task Nullable_array_Length() - { - await AssertQuery(ss => ss.Set().Where(e => e.NullableIntList.Count == 3)); - - 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 cardinality(s."NullableIntList") = 3 -"""); - } - - [ConditionalFact] - public virtual async Task Array_Length_on_EF_Property() - { - await AssertQuery(ss => ss.Set().Where(e => EF.Property>(e, nameof(ArrayEntity.IntList)).Count == 2)); - - 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 cardinality(s."IntList") = 2 -"""); - } - - #endregion Length/Count - - #region Any/All - - [ConditionalFact] - public virtual async Task Any_no_predicate() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Any())); - - 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 cardinality(s."IntList") > 0 -"""); - } - - [ConditionalFact] - public virtual async Task Any_like() - { - await AssertQuery(ss => ss.Set() - .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))), - ss => ss.Set() - .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); - - 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 s."NullableText" LIKE ANY (ARRAY['a%','b%','c%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task Any_ilike() - { - await AssertQuery(ss => ss.Set() - .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText!, p))), - ss => ss.Set() - .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); - - 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 s."NullableText" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task Any_like_anonymous() - { - await using var ctx = CreateContext(); - - var patternsActual = new List - { - "a%", - "b%", - "c%" - }; - var patternsExpected = new List - { - "a", - "b", - "c" - }; - - await AssertQuery(ss => ss.Set() - .Where(e => patternsActual.Any(p => EF.Functions.Like(e.NullableText, p))), - ss => ss.Set() - .Where(e => patternsExpected.Any(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); - - AssertSql( - """ -@patternsActual={ 'a%' -'b%' -'c%' } (DbType = Object) - -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) -"""); - } - - [ConditionalFact] - public virtual async Task All_like() - { - await AssertQuery(ss => ss.Set() - .Where(e => new List { "b%", "ba%" }.All(p => EF.Functions.Like(e.NullableText, p))), - ss => ss.Set() - .Where(e => new List { "b", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.Ordinal)))); - - 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 s."NullableText" LIKE ALL (ARRAY['b%','ba%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task All_ilike() - { - await AssertQuery(ss => ss.Set() - .Where(e => new List { "B%", "ba%" }.All(p => EF.Functions.ILike(e.NullableText!, p))), - ss => ss.Set() - .Where(e => new List { "B", "ba" }.All(p => e.NullableText!.StartsWith(p, StringComparison.OrdinalIgnoreCase)))); - - 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 s."NullableText" ILIKE ALL (ARRAY['B%','ba%']::text[]) -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_on_constant_array() - { - await AssertQuery(ss => ss.Set().Where(e => new[] { 2, 3 }.Any(p => e.IntList.Contains(p)))); - - 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[2,3]::integer[] && s."IntList" -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_between_column_and_List() - { - var ints = new List { 2, 3 }; - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Any(i => ints.Contains(i)))); - - AssertSql( - """ -@ints={ '2' -'3' } (DbType = Object) - -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 -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_between_column_and_array() - { - var ints = new[] { 2, 3 }; - await AssertQuery(ss => ss.Set().Where(e => e.IntList.Any(i => ints.Contains(i)))); - - AssertSql( - """ -@ints={ '2' -'3' } (DbType = Object) - -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 -"""); - } - - [ConditionalFact] - public virtual async Task Any_Contains_between_column_and_other_type() - { - var array = new[] { SomeEnum.Eight }; - - await AssertQuery(ss => ss.Set().Where(e => e.ValueConvertedListOfEnum.Any(i => array.Contains(i)))); - - AssertSql( - """ -@array={ 'Eight' } (DbType = Object) - -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."ValueConvertedListOfEnum" && @array -"""); - } - - [ConditionalFact] - public virtual async Task All_Contains() - { - await AssertQuery(ss => ss.Set().Where(e => new[] { 5, 6 }.All(p => e.IntList.Contains(p)))); - - 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[5,6]::integer[] <@ s."IntList" -"""); - } - - #endregion Any/All - - #region Other translations - - // TODO: https://github.com/dotnet/efcore/issues/30669 - // [ConditionalFact] - // public virtual async Task Append() - // { - - // await base.Append(async); - // - // AssertSql( - // """ - // SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" - // FROM "SomeEntities" AS s - // WHERE array_append(s."IntList", 5) = ARRAY[3,4,5]::integer[] - // """); - -// [ConditionalFact] -// public virtual async Task Concat() -// { -// await AssertQuery(ss => ss.Set() -// .Where(e => e.IntList.Concat(new[] { 5, 6 }).SequenceEqual(new[] { 3, 4, 5, 6 }))); - -// AssertSql( -// """ -// SELECT s."Id", s."ArrayContainerEntityId", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", 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" -// FROM "SomeEntities" AS s -// WHERE array_cat(s."IntList", ARRAY[5,6]::integer[]) = ARRAY[3,4,5,6]::integer[] -// """); -// } - - [ConditionalFact] - public virtual async Task Array_IndexOf1() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList.IndexOf(6) == 1)); - - 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 COALESCE(array_position(s."IntList", 6) - 1, -1) = 1 -"""); - } - - [ConditionalFact] - public virtual async Task Array_IndexOf2() - { - await AssertQuery(ss => ss.Set().Where(e => e.IntList.IndexOf(6, 1) == 1)); - - 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 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 - [ConditionalFact] - public virtual async Task String_Join_with_array_of_int_column() - { - await AssertQuery(ss => ss.Set().Where(e => string.Join(", ", e.IntList) == "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."IntList", ', ', '') = '3, 4' -"""); - } - - [ConditionalFact] - public virtual async Task String_Join_with_array_of_string_column() - { - // This is not in ArrayQueryTest because string.Join uses another overload for string[] than for List and thus - // ArrayToListReplacingExpressionVisitor won't work. - await AssertQuery(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' -"""); - } - - [ConditionalFact] - public virtual async Task String_Join_disallow_non_array_type_mapped_parameter() - { - // 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( - ss => ss.Set() - .Where(e => string.Join(", ", e.ListOfStringConvertedToDelimitedString) == "3, 4"))); - } - - #endregion Other translations - - protected void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected ArrayQueryContext CreateContext() - => Fixture.CreateContext(); - - public class ArrayListQueryFixture : ArrayQueryFixture - { - protected override string StoreName - => "ArrayListTest"; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateNpgsqlTest.cs deleted file mode 100644 index baba29db4b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonBulkUpdateNpgsqlTest.cs +++ /dev/null @@ -1,407 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonBulkUpdateNpgsqlTest( - ComplexJsonNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : ComplexJsonBulkUpdateRelationalTestBase(fixture, testOutputHelper) -{ - #region Delete - - public override async Task Delete_entity_with_associations() - { - await base.Delete_entity_with_associations(); - - AssertSql( - """ -@deletableEntity_Name='?' - -DELETE FROM "RootEntity" AS r -WHERE r."Name" = @deletableEntity_Name -"""); - } - - public override async Task Delete_required_associate() - { - await base.Delete_required_associate(); - - AssertSql(); - } - - public override async Task Delete_optional_associate() - { - await base.Delete_optional_associate(); - - AssertSql(); - } - - #endregion Delete - - #region Update properties - - public override async Task Update_property_inside_associate() - { - await base.Update_property_inside_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{String}', to_jsonb(@p)) -"""); - } - - public override async Task Update_property_inside_associate_with_special_chars() - { - await base.Update_property_inside_associate_with_special_chars(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{String}', to_jsonb('{ Some other/JSON:like text though it [isn''t]: ממש ממש לאéèéè }'::text)) -WHERE (r."RequiredAssociate" ->> 'String') = '{ this may/look:like JSON but it [isn''t]: ממש ממש לאéèéè }' -"""); - } - - public override async Task Update_property_inside_nested_associate() - { - await base.Update_property_inside_nested_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{RequiredNestedAssociate,String}', to_jsonb(@p)) -"""); - } - - public override async Task Update_property_on_projected_associate() - { - await base.Update_property_on_projected_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{String}', to_jsonb(@p)) -"""); - } - - public override async Task Update_property_on_projected_associate_with_OrderBy_Skip() - { - await base.Update_property_on_projected_associate_with_OrderBy_Skip(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_associate_with_null_required_property() - { - await base.Update_associate_with_null_required_property(); - - AssertExecuteUpdateSql(); - } - - #endregion Update properties - - #region Update association - - public override async Task Update_associate_to_parameter() - { - await base.Update_associate_to_parameter(); - - AssertExecuteUpdateSql( - """ -@complex_type_p='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = @complex_type_p -"""); - } - - public override async Task Update_nested_associate_to_parameter() - { - await base.Update_nested_associate_to_parameter(); - - AssertExecuteUpdateSql( - """ -@complex_type_p='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{RequiredNestedAssociate}', @complex_type_p) -"""); - } - - public override async Task Update_associate_to_another_associate() - { - await base.Update_associate_to_another_associate(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate" = r."RequiredAssociate" -"""); - } - - public override async Task Update_nested_associate_to_another_nested_associate() - { - await base.Update_nested_associate_to_another_nested_associate(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{OptionalNestedAssociate}', r."RequiredAssociate" -> 'RequiredNestedAssociate') -"""); - } - - public override async Task Update_associate_to_inline() - { - await base.Update_associate_to_inline(); - - AssertExecuteUpdateSql( - """ -@complex_type_p='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = @complex_type_p -"""); - } - - public override async Task Update_associate_to_inline_with_lambda() - { - await base.Update_associate_to_inline_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = '{"Id":1000,"Int":70,"Ints":[1,2,4],"Name":"Updated associate name","String":"Updated associate string","NestedCollection":[],"OptionalNestedAssociate":null,"RequiredNestedAssociate":{"Id":1000,"Int":80,"Ints":[1,2,4],"Name":"Updated nested name","String":"Updated nested string"}}' -"""); - } - - public override async Task Update_nested_associate_to_inline_with_lambda() - { - await base.Update_nested_associate_to_inline_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{RequiredNestedAssociate}', '{"Id":1000,"Int":80,"Ints":[1,2,4],"Name":"Updated nested name","String":"Updated nested string"}') -"""); - } - - public override async Task Update_associate_to_null() - { - await base.Update_associate_to_null(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate" = NULL -"""); - } - - public override async Task Update_associate_to_null_with_lambda() - { - await base.Update_associate_to_null_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate" = NULL -"""); - } - - public override async Task Update_associate_to_null_parameter() - { - await base.Update_associate_to_null_parameter(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate" = NULL -"""); - } - - public override async Task Update_required_nested_associate_to_null() - { - await base.Update_required_nested_associate_to_null(); - - AssertExecuteUpdateSql(); - } - - #endregion Update association - - #region Update collection - - public override async Task Update_collection_to_parameter() - { - await base.Update_collection_to_parameter(); - - AssertExecuteUpdateSql( - """ -@complex_type_p='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "AssociateCollection" = @complex_type_p -"""); - } - - public override async Task Update_nested_collection_to_parameter() - { - await base.Update_nested_collection_to_parameter(); - - AssertExecuteUpdateSql( - """ -@complex_type_p='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{NestedCollection}', @complex_type_p) -"""); - } - - public override async Task Update_nested_collection_to_inline_with_lambda() - { - await base.Update_nested_collection_to_inline_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{NestedCollection}', '[{"Id":1000,"Int":80,"Ints":[1,2,4],"Name":"Updated nested name1","String":"Updated nested string1"},{"Id":1001,"Int":81,"Ints":[1,2,4],"Name":"Updated nested name2","String":"Updated nested string2"}]') -"""); - } - - public override async Task Update_nested_collection_to_another_nested_collection() - { - await base.Update_nested_collection_to_another_nested_collection(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{NestedCollection}', r."OptionalAssociate" -> 'NestedCollection') -WHERE (r."OptionalAssociate") IS NOT NULL -"""); - } - - public override async Task Update_collection_referencing_the_original_collection() - { - await base.Update_collection_referencing_the_original_collection(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_inside_structural_collection() - { - await base.Update_inside_structural_collection(); - - AssertExecuteUpdateSql(); - } - - #endregion Update collection - - #region Update primitive collection - - public override async Task Update_primitive_collection_to_constant() - { - await base.Update_primitive_collection_to_constant(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{Ints}', '[1,2,4]') -"""); - } - - public override async Task Update_primitive_collection_to_parameter() - { - await base.Update_primitive_collection_to_parameter(); - - AssertExecuteUpdateSql( - """ -@ints='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{Ints}', @ints) -"""); - } - - public override async Task Update_primitive_collection_to_another_collection() - { - await base.Update_primitive_collection_to_another_collection(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{OptionalNestedAssociate,Ints}', r."RequiredAssociate" #> '{RequiredNestedAssociate,Ints}') -"""); - } - - public override async Task Update_inside_primitive_collection() - { - await base.Update_inside_primitive_collection(); - - AssertExecuteUpdateSql( - """ -@p='?' (DbType = Int32) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{Ints,1}', to_jsonb(@p)) -WHERE jsonb_array_length(r."RequiredAssociate" -> 'Ints') >= 2 -"""); - } - - #endregion Update primitive collection - - #region Multiple updates - - public override async Task Update_multiple_properties_inside_same_associate() - { - await base.Update_multiple_properties_inside_same_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' -@p0='?' (DbType = Int32) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(jsonb_set(r."RequiredAssociate", '{String}', to_jsonb(@p)), '{Int}', to_jsonb(@p0)) -"""); - } - - public override async Task Update_multiple_properties_inside_associates_and_on_entity_type() - { - await base.Update_multiple_properties_inside_associates_and_on_entity_type(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "Name" = r."Name" || 'Modified', - "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{String}', r."OptionalAssociate" -> 'String'), - "OptionalAssociate" = jsonb_set(r."OptionalAssociate", '{RequiredNestedAssociate,String}', to_jsonb(@p)) -WHERE (r."OptionalAssociate") IS NOT NULL -"""); - } - - public override async Task Update_multiple_projected_associates_via_anonymous_type() - { - await base.Update_multiple_projected_associates_via_anonymous_type(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate" = jsonb_set(r."RequiredAssociate", '{String}', r."OptionalAssociate" -> 'String'), - "OptionalAssociate" = jsonb_set(r."OptionalAssociate", '{String}', to_jsonb(@p)) -WHERE (r."OptionalAssociate") IS NOT NULL -"""); - } - - #endregion Multiple updates - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionNpgsqlTest.cs deleted file mode 100644 index 14204c04bb..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionNpgsqlTest.cs +++ /dev/null @@ -1,220 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonCollectionNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexJsonCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb - )) WITH ORDINALITY AS a) = 2 -"""); - } - - public override async Task Where() - { - await base.Where(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("Int" integer)) WITH ORDINALITY AS a - WHERE a."Int" <> 8) = 2 -"""); - } - - public override async Task OrderBy_ElementAt() - { - await base.OrderBy_ElementAt(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT a."Int" - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer - )) WITH ORDINALITY AS a - ORDER BY a."Id" NULLS FIRST - LIMIT 1 OFFSET 0) = 8 -"""); - } - - #region Distinct - - public override async Task Distinct() - { - await base.Distinct(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ( - SELECT DISTINCT a."Id", a."Int", a."Ints", a."Name", a."String", a."NestedCollection" AS c, a."OptionalNestedAssociate" AS c0, a."RequiredNestedAssociate" AS c1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb - )) WITH ORDINALITY AS a - ) AS a0) = 2 -"""); - } - - public override async Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Distinct_projected(queryTrackingBehavior); - - AssertSql(); - } - - public override async Task Distinct_over_projected_nested_collection() - { - await base.Distinct_over_projected_nested_collection(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ( - SELECT DISTINCT a."NestedCollection" AS c - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("NestedCollection" jsonb)) WITH ORDINALITY AS a - ) AS a0) = 2 -"""); - } - - public override async Task Distinct_over_projected_filtered_nested_collection() - { - await base.Distinct_over_projected_filtered_nested_collection(); - - AssertSql(); - } - - #endregion Distinct - - #region Index - - public override async Task Index_constant() - { - await base.Index_constant(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> '{0,Int}' AS integer)) = 8 -"""); - } - - public override async Task Index_parameter() - { - await base.Index_parameter(); - - AssertSql( - """ -@i='?' (DbType = Int32) - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> ARRAY[@i,'Int']::text[] AS integer)) = 8 -"""); - } - - public override async Task Index_column() - { - await base.Index_column(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> ARRAY[r."Id" - 1,'Int']::text[] AS integer)) = 8 -"""); - } - - public override async Task Index_out_of_bounds() - { - await base.Index_out_of_bounds(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> '{9999,Int}' AS integer)) = 8 -"""); - } - - #endregion Index - - #region GroupBy - - [ConditionalFact] - public override async Task GroupBy() - { - await base.GroupBy(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE 16 IN ( - SELECT COALESCE(sum(a."Int"), 0)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Int" integer, - "String" text - )) WITH ORDINALITY AS a - GROUP BY a."String" -) -"""); - } - - #endregion GroupBy - - public override async Task Select_within_Select_within_Select_with_aggregates() - { - await base.Select_within_Select_within_Select_with_aggregates(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT max(n."Int") - FROM ROWS FROM (jsonb_to_recordset(a."NestedCollection") AS ("Int" integer)) WITH ORDINALITY AS n)), 0)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("NestedCollection" jsonb)) WITH ORDINALITY AS a) -FROM "RootEntity" AS r -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonMiscellaneousNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonMiscellaneousNpgsqlTest.cs deleted file mode 100644 index cda65c84d6..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonMiscellaneousNpgsqlTest.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonMiscellaneousNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexJsonMiscellaneousRelationalTestBase(fixture, testOutputHelper) -{ - #region Simple filters - - public override async Task Where_on_associate_scalar_property() - { - await base.Where_on_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."RequiredAssociate" ->> 'Int' AS integer)) = 8 -"""); - } - - public override async Task Where_on_optional_associate_scalar_property() - { - await base.Where_on_optional_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."OptionalAssociate" ->> 'Int' AS integer)) = 8 -"""); - } - - public override async Task Where_on_nested_associate_scalar_property() - { - await base.Where_on_nested_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."RequiredAssociate" #>> '{RequiredNestedAssociate,Int}' AS integer)) = 8 -"""); - } - - #endregion Simple filters - - #region Value types - - public override async Task Where_property_on_non_nullable_value_type() - { - await base.Where_property_on_non_nullable_value_type(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."AssociateCollection", v."OptionalAssociate", v."RequiredAssociate" -FROM "ValueRootEntity" AS v -WHERE (CAST(v."RequiredAssociate" ->> 'Int' AS integer)) = 8 -"""); - } - - public override async Task Where_property_on_nullable_value_type_Value() - { - await base.Where_property_on_nullable_value_type_Value(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."AssociateCollection", v."OptionalAssociate", v."RequiredAssociate" -FROM "ValueRootEntity" AS v -WHERE (CAST(v."OptionalAssociate" ->> 'Int' AS integer)) = 8 -"""); - } - - public override async Task Where_HasValue_on_nullable_value_type() - { - await base.Where_HasValue_on_nullable_value_type(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."AssociateCollection", v."OptionalAssociate", v."RequiredAssociate" -FROM "ValueRootEntity" AS v -WHERE (v."OptionalAssociate") IS NOT NULL -"""); - } - - #endregion Value types - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonNpgsqlFixture.cs deleted file mode 100644 index 7b0fe9d065..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonNpgsqlFixture : ComplexJsonRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionNpgsqlTest.cs deleted file mode 100644 index afed687c4f..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonPrimitiveCollectionNpgsqlTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonPrimitiveCollectionNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexJsonPrimitiveCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE jsonb_array_length(r."RequiredAssociate" -> 'Ints') = 3 -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."RequiredAssociate" #>> '{Ints,0}' AS integer)) = 1 -"""); - } - - public override async Task Contains() - { - await base.Contains(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'Ints') @> to_jsonb(3) -"""); - } - - public override async Task Any_predicate() - { - await base.Any_predicate(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'Ints') @> to_jsonb(2) -"""); - } - - public override async Task Nested_Count() - { - await base.Nested_Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE jsonb_array_length(r."RequiredAssociate" #> '{RequiredNestedAssociate,Ints}') = 3 -"""); - } - - public override async Task Select_Sum() - { - await base.Select_Sum(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(i0.element::int), 0)::int - FROM jsonb_array_elements_text(r."RequiredAssociate" -> 'Ints') WITH ORDINALITY AS i0(element)) -FROM "RootEntity" AS r -WHERE ( - SELECT COALESCE(sum(i.element::int), 0)::int - FROM jsonb_array_elements_text(r."RequiredAssociate" -> 'Ints') WITH ORDINALITY AS i(element)) >= 6 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs deleted file mode 100644 index 1004c7a988..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionNpgsqlTest.cs +++ /dev/null @@ -1,376 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonProjectionNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexJsonProjectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -"""); - } - - #region Simple properties - - public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate" ->> 'String' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_property_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate" ->> 'String' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_value_type_property_on_null_associate_throws(queryTrackingBehavior); - - AssertSql( - """ -SELECT CAST(r."OptionalAssociate" ->> 'Int' AS integer) -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_property_on_null_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT CAST(r."OptionalAssociate" ->> 'Int' AS integer) -FROM "RootEntity" AS r -"""); - } - - #endregion Simple properties - - #region Non-collection - - public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate" -> 'RequiredNestedAssociate' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate" -> 'OptionalNestedAssociate' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate" -> 'RequiredNestedAssociate' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate" -> 'OptionalNestedAssociate' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_associate_via_optional_navigation(queryTrackingBehavior); - - AssertSql( - """ -SELECT r0."RequiredAssociate" -FROM "RootReferencingEntity" AS r -LEFT JOIN "RootEntity" AS r0 ON r."RootEntityId" = r0."Id" -"""); - } - - public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT CAST(r."RequiredAssociate" ->> 'Int' AS integer) -FROM "RootEntity" AS r -"""); - } - - #endregion Non-collection - - #region Collection - - public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate_collection(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."AssociateCollection" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - - public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate" -> 'NestedCollection' -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - - public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate" -> 'NestedCollection' -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - - public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_associate_collection(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Id", a."Int", a."Ints", a."Name", a."String", a."NestedCollection", a."OptionalNestedAssociate", a."RequiredNestedAssociate" -FROM "RootEntity" AS r -JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb -)) WITH ORDINALITY AS a ON TRUE -"""); - } - - public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text -)) WITH ORDINALITY AS n ON TRUE -"""); - } - - public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."OptionalAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text -)) WITH ORDINALITY AS n ON TRUE -"""); - } - - #endregion Collection - - #region Multiple - - public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_duplicated(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -"""); - } - - #endregion Multiple - - #region Subquery - - public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior); - - AssertSql( - """ -SELECT r1.c -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."RequiredAssociate" -> 'RequiredNestedAssociate' AS c - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - - public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior); - - AssertSql( - """ -SELECT r1.c -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."OptionalAssociate" -> 'RequiredNestedAssociate' AS c - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - - #endregion Subquery - - #region Value types - - public override async Task Select_root_with_value_types(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_with_value_types(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."Id", v."Name", v."AssociateCollection", v."OptionalAssociate", v."RequiredAssociate" -FROM "ValueRootEntity" AS v -"""); - } - - public override async Task Select_non_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_non_nullable_value_type(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."RequiredAssociate" -FROM "ValueRootEntity" AS v -ORDER BY v."Id" NULLS FIRST -"""); - } - - public override async Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."OptionalAssociate" -FROM "ValueRootEntity" AS v -ORDER BY v."Id" NULLS FIRST -"""); - } - - public override async Task Select_nullable_value_type_with_Value(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_with_Value(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."OptionalAssociate" -FROM "ValueRootEntity" AS v -ORDER BY v."Id" NULLS FIRST -"""); - } - - #endregion Value types - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSetOperationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSetOperationsNpgsqlTest.cs deleted file mode 100644 index 916c8ff909..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonSetOperationsNpgsqlTest.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonSetOperationsNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexJsonSetOperationsRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Over_associate_collections() - { - await base.Over_associate_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("Int" integer)) WITH ORDINALITY AS a - WHERE a."Int" = 8 - UNION ALL - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("String" text)) WITH ORDINALITY AS a0 - WHERE a0."String" = 'foo' - ) AS u) = 4 -"""); - } - - public override async Task Over_associate_collection_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Over_associate_collection_projected(queryTrackingBehavior); - - AssertSql(); - } - - public override async Task Over_assocate_collection_Select_nested_with_aggregates_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Over_assocate_collection_Select_nested_with_aggregates_projected(queryTrackingBehavior); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT COALESCE(sum(n."Int"), 0)::int - FROM ROWS FROM (jsonb_to_recordset(u."NestedCollection") AS ("Int" integer)) WITH ORDINALITY AS n)), 0)::int - FROM ( - SELECT a."NestedCollection" AS "NestedCollection" - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Int" integer, - "NestedCollection" jsonb - )) WITH ORDINALITY AS a - WHERE a."Int" = 8 - UNION ALL - SELECT a0."NestedCollection" AS "NestedCollection" - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "String" text, - "NestedCollection" jsonb - )) WITH ORDINALITY AS a0 - WHERE a0."String" = 'foo' - ) AS u) -FROM "RootEntity" AS r -"""); - } - - public override async Task Over_nested_associate_collection() - { - await base.Over_nested_associate_collection(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ("Int" integer)) WITH ORDINALITY AS n - WHERE n."Int" = 8 - UNION ALL - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ("String" text)) WITH ORDINALITY AS n0 - WHERE n0."String" = 'foo' - ) AS u) = 4 -"""); - } - - public override async Task Over_different_collection_properties() - { - await base.Over_different_collection_properties(); - - AssertSql(); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualityNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualityNpgsqlTest.cs deleted file mode 100644 index 90ad538a05..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonStructuralEqualityNpgsqlTest.cs +++ /dev/null @@ -1,288 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson; - -public class ComplexJsonStructuralEqualityNpgsqlTest(ComplexJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexJsonStructuralEqualityRelationalTestBase(fixture, testOutputHelper) -{ - // The SQL Server json type cannot be compared ("The JSON data type cannot be compared or sorted, except when using the - // IS NULL operator"). - // So we find comparisons that involve the json type, and apply a conversion to string (nvarchar(max)) to both sides. - - public override async Task Two_associates() - { - await base.Two_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate") = (r."OptionalAssociate") -"""); - } - - public override async Task Two_nested_associates() - { - await base.Two_nested_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'RequiredNestedAssociate') = (r."OptionalAssociate" -> 'RequiredNestedAssociate') -"""); - } - - public override async Task Not_equals() - { - await base.Not_equals(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate") <> (r."OptionalAssociate") OR (r."OptionalAssociate") IS NULL -"""); - } - - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."OptionalAssociate") IS NULL -"""); - } - - public override async Task Associate_with_parameter_null() - { - await base.Associate_with_parameter_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."OptionalAssociate") IS NULL -"""); - } - - public override async Task Nested_associate_with_inline_null() - { - await base.Nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" ->> 'OptionalNestedAssociate') IS NULL -"""); - } - - public override async Task Nested_associate_with_inline() - { - await base.Nested_associate_with_inline(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'RequiredNestedAssociate') = '{"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"}' -"""); - } - - public override async Task Nested_associate_with_parameter() - { - await base.Nested_associate_with_parameter(); - - AssertSql( - """ -@entity_equality_nested='?' (DbType = Object) - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'RequiredNestedAssociate') = @entity_equality_nested -"""); - } - - public override async Task Two_nested_collections() - { - await base.Two_nested_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'NestedCollection') = (r."OptionalAssociate" -> 'NestedCollection') -"""); - } - - public override async Task Nested_collection_with_inline() - { - await base.Nested_collection_with_inline(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'NestedCollection') = '[{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}]' -"""); - } - - public override async Task Nested_collection_with_parameter() - { - await base.Nested_collection_with_parameter(); - - AssertSql( - """ -@entity_equality_nestedCollection='?' (DbType = Object) - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'NestedCollection') = @entity_equality_nestedCollection -"""); - } - - #region Contains - - public override async Task Contains_with_inline() - { - await base.Contains_with_inline(); - - // TODO: The following translation is sub-optimal: we should be using OPENSJON to extract elements of the collection as JSON elements (OPENJSON WITH JSON), - // and comparison those elements to a single entire JSON fragment on the other side (just like non-collection JSON comparison), rather than breaking the - // elements down to their columns and doing column-by-column comparison. See #32576. - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE EXISTS ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS n - WHERE n."Id" = 1002 AND n."Int" = 8 AND n."Ints" = '[1,2,3]' AND n."Name" = 'Root1_RequiredAssociate_NestedCollection_1' AND n."String" = 'foo') -"""); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - // TODO: The following translation is sub-optimal: we should be using OPENSJON to extract elements of the collection as JSON elements (OPENJSON WITH JSON), - // and comparison those elements to a single entire JSON fragment on the other side (just like non-collection JSON comparison), rather than breaking the - // elements down to their columns and doing column-by-column comparison. See #32576. - AssertSql( - """ -@entity_equality_nested_Id='?' (DbType = Int32) -@entity_equality_nested_Int='?' (DbType = Int32) -@entity_equality_nested_Ints='?' (DbType = Object) -@entity_equality_nested_Name='?' -@entity_equality_nested_String='?' - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE EXISTS ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS n - WHERE n."Id" = @entity_equality_nested_Id AND n."Int" = @entity_equality_nested_Int AND n."Ints" = @entity_equality_nested_Ints AND n."Name" = @entity_equality_nested_Name AND n."String" = @entity_equality_nested_String) -"""); - } - - public override async Task Contains_with_operators_composed_on_the_collection() - { - await base.Contains_with_operators_composed_on_the_collection(); - - AssertSql( - """ -@get_Item_Int='?' (DbType = Int32) -@entity_equality_get_Item_Id='?' (DbType = Int32) -@entity_equality_get_Item_Int='?' (DbType = Int32) -@entity_equality_get_Item_Ints='?' (DbType = Object) -@entity_equality_get_Item_Name='?' -@entity_equality_get_Item_String='?' - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE EXISTS ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS n - WHERE n."Int" > @get_Item_Int AND n."Id" = @entity_equality_get_Item_Id AND n."Int" = @entity_equality_get_Item_Int AND n."Ints" = @entity_equality_get_Item_Ints AND n."Name" = @entity_equality_get_Item_Name AND n."String" = @entity_equality_get_Item_String) -"""); - } - - public override async Task Contains_with_nested_and_composed_operators() - { - await base.Contains_with_nested_and_composed_operators(); - - AssertSql( - """ -@get_Item_Id='?' (DbType = Int32) -@entity_equality_get_Item_Id='?' (DbType = Int32) -@entity_equality_get_Item_Int='?' (DbType = Int32) -@entity_equality_get_Item_Ints='?' (DbType = Object) -@entity_equality_get_Item_Name='?' -@entity_equality_get_Item_String='?' -@entity_equality_get_Item_NestedCollection='?' (DbType = Object) -@entity_equality_get_Item_OptionalNestedAssociate='?' (DbType = Object) -@entity_equality_get_Item_RequiredNestedAssociate='?' (DbType = Object) - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE EXISTS ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb - )) WITH ORDINALITY AS a - WHERE a."Id" > @get_Item_Id AND a."Id" = @entity_equality_get_Item_Id AND a."Int" = @entity_equality_get_Item_Int AND a."Ints" = @entity_equality_get_Item_Ints AND a."Name" = @entity_equality_get_Item_Name AND a."String" = @entity_equality_get_Item_String AND (a."NestedCollection") = @entity_equality_get_Item_NestedCollection AND (a."OptionalNestedAssociate") = @entity_equality_get_Item_OptionalNestedAssociate AND (a."RequiredNestedAssociate") = @entity_equality_get_Item_RequiredNestedAssociate) -"""); - } - - #endregion Contains - - #region Value types - - public override async Task Nullable_value_type_with_null() - { - await base.Nullable_value_type_with_null(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."AssociateCollection", v."OptionalAssociate", v."RequiredAssociate" -FROM "ValueRootEntity" AS v -WHERE (v."OptionalAssociate") IS NULL -"""); - } - - #endregion Value types - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingBulkUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingBulkUpdateNpgsqlTest.cs deleted file mode 100644 index e338cb80c6..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingBulkUpdateNpgsqlTest.cs +++ /dev/null @@ -1,513 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting; - -public class ComplexTableSplittingBulkUpdateNpgsqlTest( - ComplexTableSplittingNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : ComplexTableSplittingBulkUpdateRelationalTestBase(fixture, testOutputHelper) -{ - #region Delete - - public override async Task Delete_entity_with_associations() - { - await base.Delete_entity_with_associations(); - - AssertSql( - """ -@deletableEntity_Name='?' - -DELETE FROM "RootEntity" AS r -WHERE r."Name" = @deletableEntity_Name -"""); - } - - public override async Task Delete_required_associate() - { - await base.Delete_required_associate(); - - AssertSql(); - } - - public override async Task Delete_optional_associate() - { - await base.Delete_optional_associate(); - - AssertSql(); - } - - #endregion Delete - - #region Update properties - - public override async Task Update_property_inside_associate() - { - await base.Update_property_inside_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_String" = @p -"""); - } - - public override async Task Update_property_inside_associate_with_special_chars() - { - await base.Update_property_inside_associate_with_special_chars(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate_String" = '{ Some other/JSON:like text though it [isn''t]: ממש ממש לאéèéè }' -WHERE r."RequiredAssociate_String" = '{ this may/look:like JSON but it [isn''t]: ממש ממש לאéèéè }' -"""); - } - - public override async Task Update_property_inside_nested_associate() - { - await base.Update_property_inside_nested_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_RequiredNestedAssociate_String" = @p -"""); - } - - public override async Task Update_property_on_projected_associate() - { - await base.Update_property_on_projected_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_String" = @p -"""); - } - - public override async Task Update_property_on_projected_associate_with_OrderBy_Skip() - { - await base.Update_property_on_projected_associate_with_OrderBy_Skip(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_associate_with_null_required_property() - { - await base.Update_associate_with_null_required_property(); - - AssertExecuteUpdateSql(); - } - - #endregion Update properties - - #region Update association - - public override async Task Update_associate_to_parameter() - { - await base.Update_associate_to_parameter(); - - AssertExecuteUpdateSql( - """ -@complex_type_p_Id='?' (DbType = Int32) -@complex_type_p_Int='?' (DbType = Int32) -@complex_type_p_Ints='?' (DbType = Object) -@complex_type_p_Name='?' -@complex_type_p_String='?' -@complex_type_p_RequiredNestedAssociate_Id='?' (DbType = Int32) -@complex_type_p_RequiredNestedAssociate_Int='?' (DbType = Int32) -@complex_type_p_RequiredNestedAssociate_Ints='?' (DbType = Object) -@complex_type_p_RequiredNestedAssociate_Name='?' -@complex_type_p_RequiredNestedAssociate_String='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_Id" = @complex_type_p_Id, - "RequiredAssociate_Int" = @complex_type_p_Int, - "RequiredAssociate_Ints" = @complex_type_p_Ints, - "RequiredAssociate_Name" = @complex_type_p_Name, - "RequiredAssociate_String" = @complex_type_p_String, - "RequiredAssociate_OptionalNestedAssociate_Id" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Int" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Ints" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Name" = NULL, - "RequiredAssociate_OptionalNestedAssociate_String" = NULL, - "RequiredAssociate_RequiredNestedAssociate_Id" = @complex_type_p_RequiredNestedAssociate_Id, - "RequiredAssociate_RequiredNestedAssociate_Int" = @complex_type_p_RequiredNestedAssociate_Int, - "RequiredAssociate_RequiredNestedAssociate_Ints" = @complex_type_p_RequiredNestedAssociate_Ints, - "RequiredAssociate_RequiredNestedAssociate_Name" = @complex_type_p_RequiredNestedAssociate_Name, - "RequiredAssociate_RequiredNestedAssociate_String" = @complex_type_p_RequiredNestedAssociate_String -"""); - } - - public override async Task Update_nested_associate_to_parameter() - { - await base.Update_nested_associate_to_parameter(); - - AssertExecuteUpdateSql( - """ -@complex_type_p_Id='?' (DbType = Int32) -@complex_type_p_Int='?' (DbType = Int32) -@complex_type_p_Ints='?' (DbType = Object) -@complex_type_p_Name='?' -@complex_type_p_String='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_RequiredNestedAssociate_Id" = @complex_type_p_Id, - "RequiredAssociate_RequiredNestedAssociate_Int" = @complex_type_p_Int, - "RequiredAssociate_RequiredNestedAssociate_Ints" = @complex_type_p_Ints, - "RequiredAssociate_RequiredNestedAssociate_Name" = @complex_type_p_Name, - "RequiredAssociate_RequiredNestedAssociate_String" = @complex_type_p_String -"""); - } - - public override async Task Update_associate_to_another_associate() - { - await base.Update_associate_to_another_associate(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate_Id" = r."RequiredAssociate_Id", - "OptionalAssociate_Int" = r."RequiredAssociate_Int", - "OptionalAssociate_Ints" = r."RequiredAssociate_Ints", - "OptionalAssociate_Name" = r."RequiredAssociate_Name", - "OptionalAssociate_String" = r."RequiredAssociate_String", - "OptionalAssociate_OptionalNestedAssociate_Id" = r."OptionalAssociate_OptionalNestedAssociate_Id", - "OptionalAssociate_OptionalNestedAssociate_Int" = r."OptionalAssociate_OptionalNestedAssociate_Int", - "OptionalAssociate_OptionalNestedAssociate_Ints" = r."OptionalAssociate_OptionalNestedAssociate_Ints", - "OptionalAssociate_OptionalNestedAssociate_Name" = r."OptionalAssociate_OptionalNestedAssociate_Name", - "OptionalAssociate_OptionalNestedAssociate_String" = r."OptionalAssociate_OptionalNestedAssociate_String", - "OptionalAssociate_RequiredNestedAssociate_Id" = r."OptionalAssociate_RequiredNestedAssociate_Id", - "OptionalAssociate_RequiredNestedAssociate_Int" = r."OptionalAssociate_RequiredNestedAssociate_Int", - "OptionalAssociate_RequiredNestedAssociate_Ints" = r."OptionalAssociate_RequiredNestedAssociate_Ints", - "OptionalAssociate_RequiredNestedAssociate_Name" = r."OptionalAssociate_RequiredNestedAssociate_Name", - "OptionalAssociate_RequiredNestedAssociate_String" = r."OptionalAssociate_RequiredNestedAssociate_String" -"""); - } - - public override async Task Update_nested_associate_to_another_nested_associate() - { - await base.Update_nested_associate_to_another_nested_associate(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate_OptionalNestedAssociate_Id" = r."RequiredAssociate_RequiredNestedAssociate_Id", - "RequiredAssociate_OptionalNestedAssociate_Int" = r."RequiredAssociate_RequiredNestedAssociate_Int", - "RequiredAssociate_OptionalNestedAssociate_Ints" = r."RequiredAssociate_RequiredNestedAssociate_Ints", - "RequiredAssociate_OptionalNestedAssociate_Name" = r."RequiredAssociate_RequiredNestedAssociate_Name", - "RequiredAssociate_OptionalNestedAssociate_String" = r."RequiredAssociate_RequiredNestedAssociate_String" -"""); - } - - public override async Task Update_associate_to_inline() - { - await base.Update_associate_to_inline(); - - AssertExecuteUpdateSql( - """ -@complex_type_p_Id='?' (DbType = Int32) -@complex_type_p_Int='?' (DbType = Int32) -@complex_type_p_Ints='?' (DbType = Object) -@complex_type_p_Name='?' -@complex_type_p_String='?' -@complex_type_p_RequiredNestedAssociate_Id='?' (DbType = Int32) -@complex_type_p_RequiredNestedAssociate_Int='?' (DbType = Int32) -@complex_type_p_RequiredNestedAssociate_Ints='?' (DbType = Object) -@complex_type_p_RequiredNestedAssociate_Name='?' -@complex_type_p_RequiredNestedAssociate_String='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_Id" = @complex_type_p_Id, - "RequiredAssociate_Int" = @complex_type_p_Int, - "RequiredAssociate_Ints" = @complex_type_p_Ints, - "RequiredAssociate_Name" = @complex_type_p_Name, - "RequiredAssociate_String" = @complex_type_p_String, - "RequiredAssociate_OptionalNestedAssociate_Id" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Int" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Ints" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Name" = NULL, - "RequiredAssociate_OptionalNestedAssociate_String" = NULL, - "RequiredAssociate_RequiredNestedAssociate_Id" = @complex_type_p_RequiredNestedAssociate_Id, - "RequiredAssociate_RequiredNestedAssociate_Int" = @complex_type_p_RequiredNestedAssociate_Int, - "RequiredAssociate_RequiredNestedAssociate_Ints" = @complex_type_p_RequiredNestedAssociate_Ints, - "RequiredAssociate_RequiredNestedAssociate_Name" = @complex_type_p_RequiredNestedAssociate_Name, - "RequiredAssociate_RequiredNestedAssociate_String" = @complex_type_p_RequiredNestedAssociate_String -"""); - } - - public override async Task Update_associate_to_inline_with_lambda() - { - await base.Update_associate_to_inline_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate_Id" = 1000, - "RequiredAssociate_Int" = 70, - "RequiredAssociate_Ints" = ARRAY[1,2,4]::integer[], - "RequiredAssociate_Name" = 'Updated associate name', - "RequiredAssociate_String" = 'Updated associate string', - "RequiredAssociate_OptionalNestedAssociate_Id" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Int" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Ints" = NULL, - "RequiredAssociate_OptionalNestedAssociate_Name" = NULL, - "RequiredAssociate_OptionalNestedAssociate_String" = NULL, - "RequiredAssociate_RequiredNestedAssociate_Id" = 1000, - "RequiredAssociate_RequiredNestedAssociate_Int" = 80, - "RequiredAssociate_RequiredNestedAssociate_Ints" = ARRAY[1,2,4]::integer[], - "RequiredAssociate_RequiredNestedAssociate_Name" = 'Updated nested name', - "RequiredAssociate_RequiredNestedAssociate_String" = 'Updated nested string' -"""); - } - - public override async Task Update_nested_associate_to_inline_with_lambda() - { - await base.Update_nested_associate_to_inline_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate_RequiredNestedAssociate_Id" = 1000, - "RequiredAssociate_RequiredNestedAssociate_Int" = 80, - "RequiredAssociate_RequiredNestedAssociate_Ints" = ARRAY[1,2,4]::integer[], - "RequiredAssociate_RequiredNestedAssociate_Name" = 'Updated nested name', - "RequiredAssociate_RequiredNestedAssociate_String" = 'Updated nested string' -"""); - } - - public override async Task Update_associate_to_null() - { - await base.Update_associate_to_null(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate_Id" = NULL, - "OptionalAssociate_Int" = NULL, - "OptionalAssociate_Ints" = NULL, - "OptionalAssociate_Name" = NULL, - "OptionalAssociate_String" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Id" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Int" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Ints" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Name" = NULL, - "OptionalAssociate_OptionalNestedAssociate_String" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Id" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Int" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Ints" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Name" = NULL, - "OptionalAssociate_RequiredNestedAssociate_String" = NULL -"""); - } - - public override async Task Update_associate_to_null_with_lambda() - { - await base.Update_associate_to_null_with_lambda(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate_Id" = NULL, - "OptionalAssociate_Int" = NULL, - "OptionalAssociate_Ints" = NULL, - "OptionalAssociate_Name" = NULL, - "OptionalAssociate_String" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Id" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Int" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Ints" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Name" = NULL, - "OptionalAssociate_OptionalNestedAssociate_String" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Id" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Int" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Ints" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Name" = NULL, - "OptionalAssociate_RequiredNestedAssociate_String" = NULL -"""); - } - - public override async Task Update_associate_to_null_parameter() - { - await base.Update_associate_to_null_parameter(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "OptionalAssociate_Id" = NULL, - "OptionalAssociate_Int" = NULL, - "OptionalAssociate_Ints" = NULL, - "OptionalAssociate_Name" = NULL, - "OptionalAssociate_String" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Id" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Int" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Ints" = NULL, - "OptionalAssociate_OptionalNestedAssociate_Name" = NULL, - "OptionalAssociate_OptionalNestedAssociate_String" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Id" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Int" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Ints" = NULL, - "OptionalAssociate_RequiredNestedAssociate_Name" = NULL, - "OptionalAssociate_RequiredNestedAssociate_String" = NULL -"""); - } - - public override async Task Update_required_nested_associate_to_null() - { - await base.Update_required_nested_associate_to_null(); - - AssertExecuteUpdateSql(); - } - - #endregion Update association - - #region Update collection - - public override async Task Update_collection_to_parameter() - { - await base.Update_collection_to_parameter(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_nested_collection_to_parameter() - { - await base.Update_nested_collection_to_parameter(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_nested_collection_to_inline_with_lambda() - { - await base.Update_nested_collection_to_inline_with_lambda(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_nested_collection_to_another_nested_collection() - { - await base.Update_nested_collection_to_another_nested_collection(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_collection_referencing_the_original_collection() - { - await base.Update_collection_referencing_the_original_collection(); - - AssertExecuteUpdateSql(); - } - - public override async Task Update_inside_structural_collection() - { - await base.Update_inside_structural_collection(); - - AssertExecuteUpdateSql(); - } - - #endregion Update collection - - #region Update primitive collection - - public override async Task Update_primitive_collection_to_constant() - { - await base.Update_primitive_collection_to_constant(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate_Ints" = ARRAY[1,2,4]::integer[] -"""); - } - - public override async Task Update_primitive_collection_to_parameter() - { - await base.Update_primitive_collection_to_parameter(); - - AssertExecuteUpdateSql( - """ -@ints='?' (DbType = Object) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_Ints" = @ints -"""); - } - - public override async Task Update_primitive_collection_to_another_collection() - { - await base.Update_primitive_collection_to_another_collection(); - - AssertExecuteUpdateSql( - """ -UPDATE "RootEntity" AS r -SET "RequiredAssociate_OptionalNestedAssociate_Ints" = r."RequiredAssociate_RequiredNestedAssociate_Ints" -"""); - } - - public override async Task Update_inside_primitive_collection() - { - // #3622, Support updating an element in an array with ExecuteUpdate - await Assert.ThrowsAsync(() => base.Update_inside_primitive_collection()); - - AssertExecuteUpdateSql(); - } - - #endregion Update primitive collection - - #region Multiple updates - - public override async Task Update_multiple_properties_inside_same_associate() - { - await base.Update_multiple_properties_inside_same_associate(); - - AssertExecuteUpdateSql( - """ -@p='?' -@p0='?' (DbType = Int32) - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_String" = @p, - "RequiredAssociate_Int" = @p0 -"""); - } - - public override async Task Update_multiple_properties_inside_associates_and_on_entity_type() - { - await base.Update_multiple_properties_inside_associates_and_on_entity_type(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "Name" = r."Name" || 'Modified', - "RequiredAssociate_String" = r."OptionalAssociate_String", - "OptionalAssociate_RequiredNestedAssociate_String" = @p -WHERE r."OptionalAssociate_Id" IS NOT NULL -"""); - } - - public override async Task Update_multiple_projected_associates_via_anonymous_type() - { - await base.Update_multiple_projected_associates_via_anonymous_type(); - - AssertExecuteUpdateSql( - """ -@p='?' - -UPDATE "RootEntity" AS r -SET "RequiredAssociate_String" = r."OptionalAssociate_String", - "OptionalAssociate_String" = @p -WHERE r."OptionalAssociate_Id" IS NOT NULL -"""); - } - - #endregion Multiple updates - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingMiscellaneousNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingMiscellaneousNpgsqlTest.cs deleted file mode 100644 index 7c32a1764a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingMiscellaneousNpgsqlTest.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting; - -public class ComplexTableSplittingMiscellaneousNpgsqlTest( - ComplexTableSplittingNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : ComplexTableSplittingMiscellaneousRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Where_on_associate_scalar_property() - { - await base.Where_on_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_Int" = 8 -"""); - } - - public override async Task Where_on_optional_associate_scalar_property() - { - await base.Where_on_optional_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."OptionalAssociate_Int" = 8 -"""); - } - - public override async Task Where_on_nested_associate_scalar_property() - { - await base.Where_on_nested_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_RequiredNestedAssociate_Int" = 8 -"""); - } - - #region Value types - - public override async Task Where_property_on_non_nullable_value_type() - { - await base.Where_property_on_non_nullable_value_type(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String", v."RequiredAssociate_Id", v."RequiredAssociate_Int", v."RequiredAssociate_Name", v."RequiredAssociate_String", v."RequiredAssociate_OptionalNested_Id", v."RequiredAssociate_OptionalNested_Int", v."RequiredAssociate_OptionalNested_Name", v."RequiredAssociate_OptionalNested_String", v."RequiredAssociate_RequiredNested_Id", v."RequiredAssociate_RequiredNested_Int", v."RequiredAssociate_RequiredNested_Name", v."RequiredAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -WHERE v."RequiredAssociate_Int" = 8 -"""); - } - - public override async Task Where_property_on_nullable_value_type_Value() - { - await base.Where_property_on_nullable_value_type_Value(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String", v."RequiredAssociate_Id", v."RequiredAssociate_Int", v."RequiredAssociate_Name", v."RequiredAssociate_String", v."RequiredAssociate_OptionalNested_Id", v."RequiredAssociate_OptionalNested_Int", v."RequiredAssociate_OptionalNested_Name", v."RequiredAssociate_OptionalNested_String", v."RequiredAssociate_RequiredNested_Id", v."RequiredAssociate_RequiredNested_Int", v."RequiredAssociate_RequiredNested_Name", v."RequiredAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -WHERE v."OptionalAssociate_Int" = 8 -"""); - } - - public override async Task Where_HasValue_on_nullable_value_type() - { - await base.Where_HasValue_on_nullable_value_type(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String", v."RequiredAssociate_Id", v."RequiredAssociate_Int", v."RequiredAssociate_Name", v."RequiredAssociate_String", v."RequiredAssociate_OptionalNested_Id", v."RequiredAssociate_OptionalNested_Int", v."RequiredAssociate_OptionalNested_Name", v."RequiredAssociate_OptionalNested_String", v."RequiredAssociate_RequiredNested_Id", v."RequiredAssociate_RequiredNested_Int", v."RequiredAssociate_RequiredNested_Name", v."RequiredAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -WHERE v."OptionalAssociate_Id" IS NOT NULL -"""); - } - - #endregion Value types - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingNpgsqlFixture.cs deleted file mode 100644 index 2896066e97..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting; - -public class ComplexTableSplittingNpgsqlFixture : ComplexTableSplittingRelationalFixtureBase, ITestSqlLoggerFactory -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingPrimitiveCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingPrimitiveCollectionNpgsqlTest.cs deleted file mode 100644 index 1ff527a6b5..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingPrimitiveCollectionNpgsqlTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting; - -public class ComplexTableSplittingPrimitiveCollectionNpgsqlTest(ComplexTableSplittingNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : ComplexTableSplittingPrimitiveCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE cardinality(r."RequiredAssociate_Ints") = 3 -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_Ints"[1] = 1 -"""); - } - - public override async Task Contains() - { - await base.Contains(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE 3 = ANY (r."RequiredAssociate_Ints") -"""); - } - - public override async Task Any_predicate() - { - await base.Any_predicate(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE 2 = ANY (r."RequiredAssociate_Ints") -"""); - } - - public override async Task Nested_Count() - { - await base.Nested_Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE cardinality(r."RequiredAssociate_RequiredNestedAssociate_Ints") = 3 -"""); - } - - public override async Task Select_Sum() - { - await base.Select_Sum(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(r1.value), 0)::int - FROM unnest(r."RequiredAssociate_Ints") AS r1(value)) -FROM "RootEntity" AS r -WHERE ( - SELECT COALESCE(sum(r0.value), 0)::int - FROM unnest(r."RequiredAssociate_Ints") AS r0(value)) >= 6 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs deleted file mode 100644 index 87a40359b1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingProjectionNpgsqlTest.cs +++ /dev/null @@ -1,345 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting; - -public class ComplexTableSplittingProjectionNpgsqlTest( - ComplexTableSplittingNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : ComplexTableSplittingProjectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - #region Scalar properties - - public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_property_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_value_type_property_on_null_associate_throws(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_Int" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_property_on_null_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_Int" -FROM "RootEntity" AS r -"""); - } - - #endregion Scalar properties - - #region Structural properties - - public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_associate_via_optional_navigation(queryTrackingBehavior); - - AssertSql( - """ -SELECT r0."RequiredAssociate_Id", r0."RequiredAssociate_Int", r0."RequiredAssociate_Ints", r0."RequiredAssociate_Name", r0."RequiredAssociate_String", r0."RequiredAssociate_OptionalNestedAssociate_Id", r0."RequiredAssociate_OptionalNestedAssociate_Int", r0."RequiredAssociate_OptionalNestedAssociate_Ints", r0."RequiredAssociate_OptionalNestedAssociate_Name", r0."RequiredAssociate_OptionalNestedAssociate_String", r0."RequiredAssociate_RequiredNestedAssociate_Id", r0."RequiredAssociate_RequiredNestedAssociate_Int", r0."RequiredAssociate_RequiredNestedAssociate_Ints", r0."RequiredAssociate_RequiredNestedAssociate_Name", r0."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootReferencingEntity" AS r -LEFT JOIN "RootEntity" AS r0 ON r."RootEntityId" = r0."Id" -"""); - } - - public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_Int" -FROM "RootEntity" AS r -"""); - } - - #endregion Structural properties - - #region Structural collection properties - - public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate_collection(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - - public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - - public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - - public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_associate_collection(queryTrackingBehavior); - - AssertSql( -); - } - - public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); - - AssertSql( -); - } - - public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); - - AssertSql( -); - } - - #endregion Structural collection properties - - #region Multiple - - public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_duplicated(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - - #endregion Multiple - - #region Subquery - - public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior); - - AssertSql( - """ -SELECT r1."RequiredAssociate_RequiredNestedAssociate_Id", r1."RequiredAssociate_RequiredNestedAssociate_Int", r1."RequiredAssociate_RequiredNestedAssociate_Ints", r1."RequiredAssociate_RequiredNestedAssociate_Name", r1."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."RequiredAssociate_RequiredNestedAssociate_Id", r0."RequiredAssociate_RequiredNestedAssociate_Int", r0."RequiredAssociate_RequiredNestedAssociate_Ints", r0."RequiredAssociate_RequiredNestedAssociate_Name", r0."RequiredAssociate_RequiredNestedAssociate_String" - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - - public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior); - - AssertSql( - """ -SELECT r1."OptionalAssociate_RequiredNestedAssociate_Id", r1."OptionalAssociate_RequiredNestedAssociate_Int", r1."OptionalAssociate_RequiredNestedAssociate_Ints", r1."OptionalAssociate_RequiredNestedAssociate_Name", r1."OptionalAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."OptionalAssociate_RequiredNestedAssociate_Id", r0."OptionalAssociate_RequiredNestedAssociate_Int", r0."OptionalAssociate_RequiredNestedAssociate_Ints", r0."OptionalAssociate_RequiredNestedAssociate_Name", r0."OptionalAssociate_RequiredNestedAssociate_String" - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - - #endregion Subquery - - #region Value types - - public override async Task Select_root_with_value_types(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_with_value_types(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."Id", v."Name", v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String", v."RequiredAssociate_Id", v."RequiredAssociate_Int", v."RequiredAssociate_Name", v."RequiredAssociate_String", v."RequiredAssociate_OptionalNested_Id", v."RequiredAssociate_OptionalNested_Int", v."RequiredAssociate_OptionalNested_Name", v."RequiredAssociate_OptionalNested_String", v."RequiredAssociate_RequiredNested_Id", v."RequiredAssociate_RequiredNested_Int", v."RequiredAssociate_RequiredNested_Name", v."RequiredAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -"""); - } - - public override async Task Select_non_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_non_nullable_value_type(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."RequiredAssociate_Id", v."RequiredAssociate_Int", v."RequiredAssociate_Name", v."RequiredAssociate_String", v."RequiredAssociate_OptionalNested_Id", v."RequiredAssociate_OptionalNested_Int", v."RequiredAssociate_OptionalNested_Name", v."RequiredAssociate_OptionalNested_String", v."RequiredAssociate_RequiredNested_Id", v."RequiredAssociate_RequiredNested_Int", v."RequiredAssociate_RequiredNested_Name", v."RequiredAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -ORDER BY v."Id" NULLS FIRST -"""); - } - - public override async Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -ORDER BY v."Id" NULLS FIRST -"""); - } - - public override async Task Select_nullable_value_type_with_Value(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_with_Value(queryTrackingBehavior); - - AssertSql( - """ -SELECT v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -ORDER BY v."Id" NULLS FIRST -"""); - } - - #endregion Value types - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualityNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualityNpgsqlTest.cs deleted file mode 100644 index 6cb2d912c0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/ComplexTableSplitting/ComplexTableSplittingStructuralEqualityNpgsqlTest.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexTableSplitting; - -public class ComplexTableSplittingStructuralEqualityNpgsqlTest( - ComplexTableSplittingNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : ComplexTableSplittingStructuralEqualityRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Two_associates() - { - await base.Two_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_Id" = r."OptionalAssociate_Id" AND r."RequiredAssociate_Int" = r."OptionalAssociate_Int" AND r."RequiredAssociate_Ints" = r."OptionalAssociate_Ints" AND r."RequiredAssociate_Name" = r."OptionalAssociate_Name" AND r."RequiredAssociate_String" = r."OptionalAssociate_String" AND (r."RequiredAssociate_OptionalNestedAssociate_Id" = r."RequiredAssociate_OptionalNestedAssociate_Id" OR r."RequiredAssociate_OptionalNestedAssociate_Id" IS NULL) AND (r."RequiredAssociate_OptionalNestedAssociate_Int" = r."RequiredAssociate_OptionalNestedAssociate_Int" OR r."RequiredAssociate_OptionalNestedAssociate_Int" IS NULL) AND (r."RequiredAssociate_OptionalNestedAssociate_Ints" = r."RequiredAssociate_OptionalNestedAssociate_Ints" OR r."RequiredAssociate_OptionalNestedAssociate_Ints" IS NULL) AND (r."RequiredAssociate_OptionalNestedAssociate_Name" = r."RequiredAssociate_OptionalNestedAssociate_Name" OR r."RequiredAssociate_OptionalNestedAssociate_Name" IS NULL) AND (r."RequiredAssociate_OptionalNestedAssociate_String" = r."RequiredAssociate_OptionalNestedAssociate_String" OR r."RequiredAssociate_OptionalNestedAssociate_String" IS NULL) AND r."RequiredAssociate_RequiredNestedAssociate_Id" = r."RequiredAssociate_RequiredNestedAssociate_Id" AND r."RequiredAssociate_RequiredNestedAssociate_Int" = r."RequiredAssociate_RequiredNestedAssociate_Int" AND r."RequiredAssociate_RequiredNestedAssociate_Ints" = r."RequiredAssociate_RequiredNestedAssociate_Ints" AND r."RequiredAssociate_RequiredNestedAssociate_Name" = r."RequiredAssociate_RequiredNestedAssociate_Name" AND r."RequiredAssociate_RequiredNestedAssociate_String" = r."RequiredAssociate_RequiredNestedAssociate_String" -"""); - } - - public override async Task Two_nested_associates() - { - await base.Two_nested_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_RequiredNestedAssociate_Id" = r."OptionalAssociate_RequiredNestedAssociate_Id" AND r."RequiredAssociate_RequiredNestedAssociate_Int" = r."OptionalAssociate_RequiredNestedAssociate_Int" AND r."RequiredAssociate_RequiredNestedAssociate_Ints" = r."OptionalAssociate_RequiredNestedAssociate_Ints" AND r."RequiredAssociate_RequiredNestedAssociate_Name" = r."OptionalAssociate_RequiredNestedAssociate_Name" AND r."RequiredAssociate_RequiredNestedAssociate_String" = r."OptionalAssociate_RequiredNestedAssociate_String" -"""); - } - - public override async Task Not_equals() - { - await base.Not_equals(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_Id" <> r."OptionalAssociate_Id" OR r."OptionalAssociate_Id" IS NULL OR r."RequiredAssociate_Int" <> r."OptionalAssociate_Int" OR r."OptionalAssociate_Int" IS NULL OR r."RequiredAssociate_Ints" <> r."OptionalAssociate_Ints" OR r."OptionalAssociate_Ints" IS NULL OR r."RequiredAssociate_Name" <> r."OptionalAssociate_Name" OR r."OptionalAssociate_Name" IS NULL OR r."RequiredAssociate_String" <> r."OptionalAssociate_String" OR r."OptionalAssociate_String" IS NULL OR ((r."RequiredAssociate_OptionalNestedAssociate_Id" <> r."RequiredAssociate_OptionalNestedAssociate_Id" OR r."RequiredAssociate_OptionalNestedAssociate_Id" IS NULL) AND r."RequiredAssociate_OptionalNestedAssociate_Id" IS NOT NULL) OR ((r."RequiredAssociate_OptionalNestedAssociate_Int" <> r."RequiredAssociate_OptionalNestedAssociate_Int" OR r."RequiredAssociate_OptionalNestedAssociate_Int" IS NULL) AND r."RequiredAssociate_OptionalNestedAssociate_Int" IS NOT NULL) OR ((r."RequiredAssociate_OptionalNestedAssociate_Ints" <> r."RequiredAssociate_OptionalNestedAssociate_Ints" OR r."RequiredAssociate_OptionalNestedAssociate_Ints" IS NULL) AND r."RequiredAssociate_OptionalNestedAssociate_Ints" IS NOT NULL) OR ((r."RequiredAssociate_OptionalNestedAssociate_Name" <> r."RequiredAssociate_OptionalNestedAssociate_Name" OR r."RequiredAssociate_OptionalNestedAssociate_Name" IS NULL) AND r."RequiredAssociate_OptionalNestedAssociate_Name" IS NOT NULL) OR ((r."RequiredAssociate_OptionalNestedAssociate_String" <> r."RequiredAssociate_OptionalNestedAssociate_String" OR r."RequiredAssociate_OptionalNestedAssociate_String" IS NULL) AND r."RequiredAssociate_OptionalNestedAssociate_String" IS NOT NULL) OR r."RequiredAssociate_RequiredNestedAssociate_Id" <> r."RequiredAssociate_RequiredNestedAssociate_Id" OR r."RequiredAssociate_RequiredNestedAssociate_Id" IS NULL OR r."RequiredAssociate_RequiredNestedAssociate_Int" <> r."RequiredAssociate_RequiredNestedAssociate_Int" OR r."RequiredAssociate_RequiredNestedAssociate_Int" IS NULL OR r."RequiredAssociate_RequiredNestedAssociate_Ints" <> r."RequiredAssociate_RequiredNestedAssociate_Ints" OR r."RequiredAssociate_RequiredNestedAssociate_Ints" IS NULL OR r."RequiredAssociate_RequiredNestedAssociate_Name" <> r."RequiredAssociate_RequiredNestedAssociate_Name" OR r."RequiredAssociate_RequiredNestedAssociate_Name" IS NULL OR r."RequiredAssociate_RequiredNestedAssociate_String" <> r."RequiredAssociate_RequiredNestedAssociate_String" OR r."RequiredAssociate_RequiredNestedAssociate_String" IS NULL -"""); - } - - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."OptionalAssociate_Id" IS NULL -"""); - } - - public override async Task Associate_with_parameter_null() - { - await base.Associate_with_parameter_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."OptionalAssociate_Id" IS NULL AND r."OptionalAssociate_Int" IS NULL AND r."OptionalAssociate_Ints" IS NULL AND r."OptionalAssociate_Name" IS NULL AND r."OptionalAssociate_String" IS NULL AND r."OptionalAssociate_OptionalNestedAssociate_Id" IS NULL AND r."OptionalAssociate_OptionalNestedAssociate_Int" IS NULL AND r."OptionalAssociate_OptionalNestedAssociate_Ints" IS NULL AND r."OptionalAssociate_OptionalNestedAssociate_Name" IS NULL AND r."OptionalAssociate_OptionalNestedAssociate_String" IS NULL AND r."OptionalAssociate_RequiredNestedAssociate_Id" IS NULL AND r."OptionalAssociate_RequiredNestedAssociate_Int" IS NULL AND r."OptionalAssociate_RequiredNestedAssociate_Ints" IS NULL AND r."OptionalAssociate_RequiredNestedAssociate_Name" IS NULL AND r."OptionalAssociate_RequiredNestedAssociate_String" IS NULL -"""); - } - - public override async Task Nested_associate_with_inline_null() - { - await base.Nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_OptionalNestedAssociate_Id" IS NULL -"""); - } - - public override async Task Nested_associate_with_inline() - { - await base.Nested_associate_with_inline(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_RequiredNestedAssociate_Id" = 1000 AND r."RequiredAssociate_RequiredNestedAssociate_Int" = 8 AND r."RequiredAssociate_RequiredNestedAssociate_Ints" = ARRAY[1,2,3]::integer[] AND r."RequiredAssociate_RequiredNestedAssociate_Name" = 'Root1_RequiredAssociate_RequiredNestedAssociate' AND r."RequiredAssociate_RequiredNestedAssociate_String" = 'foo' -"""); - } - - public override async Task Nested_associate_with_parameter() - { - await base.Nested_associate_with_parameter(); - - AssertSql( - """ -@entity_equality_nested_Id='?' (DbType = Int32) -@entity_equality_nested_Int='?' (DbType = Int32) -@entity_equality_nested_Ints='?' (DbType = Object) -@entity_equality_nested_Name='?' -@entity_equality_nested_String='?' - -SELECT r."Id", r."Name", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -WHERE r."RequiredAssociate_RequiredNestedAssociate_Id" = @entity_equality_nested_Id AND r."RequiredAssociate_RequiredNestedAssociate_Int" = @entity_equality_nested_Int AND r."RequiredAssociate_RequiredNestedAssociate_Ints" = @entity_equality_nested_Ints AND r."RequiredAssociate_RequiredNestedAssociate_Name" = @entity_equality_nested_Name AND r."RequiredAssociate_RequiredNestedAssociate_String" = @entity_equality_nested_String -"""); - } - - public override async Task Two_nested_collections() - { - await base.Two_nested_collections(); - - AssertSql(); - } - - public override async Task Nested_collection_with_inline() - { - await base.Nested_collection_with_inline(); - - AssertSql(); - } - - public override async Task Nested_collection_with_parameter() - { - await base.Nested_collection_with_parameter(); - - AssertSql(); - } - - #region Contains - - public override async Task Contains_with_inline() - { - await base.Contains_with_inline(); - - AssertSql(); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - AssertSql(); - } - - public override async Task Contains_with_operators_composed_on_the_collection() - { - await base.Contains_with_operators_composed_on_the_collection(); - - AssertSql(); - } - - public override async Task Contains_with_nested_and_composed_operators() - { - await base.Contains_with_nested_and_composed_operators(); - - AssertSql(); - } - - #endregion Contains - - #region Value types - - public override async Task Nullable_value_type_with_null() - { - await base.Nullable_value_type_with_null(); - - AssertSql( - """ -SELECT v."Id", v."Name", v."OptionalAssociate_Id", v."OptionalAssociate_Int", v."OptionalAssociate_Name", v."OptionalAssociate_String", v."OptionalAssociate_OptionalNested_Id", v."OptionalAssociate_OptionalNested_Int", v."OptionalAssociate_OptionalNested_Name", v."OptionalAssociate_OptionalNested_String", v."OptionalAssociate_RequiredNested_Id", v."OptionalAssociate_RequiredNested_Int", v."OptionalAssociate_RequiredNested_Name", v."OptionalAssociate_RequiredNested_String", v."RequiredAssociate_Id", v."RequiredAssociate_Int", v."RequiredAssociate_Name", v."RequiredAssociate_String", v."RequiredAssociate_OptionalNested_Id", v."RequiredAssociate_OptionalNested_Int", v."RequiredAssociate_OptionalNested_Name", v."RequiredAssociate_OptionalNested_String", v."RequiredAssociate_RequiredNested_Id", v."RequiredAssociate_RequiredNested_Int", v."RequiredAssociate_RequiredNested_Name", v."RequiredAssociate_RequiredNested_String" -FROM "ValueRootEntity" AS v -WHERE v."OptionalAssociate_Id" IS NULL -"""); - } - - #endregion Value types - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionNpgsqlTest.cs deleted file mode 100644 index 923a99ff1b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionNpgsqlTest.cs +++ /dev/null @@ -1,269 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsCollectionNpgsqlTest(NavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : NavigationsCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a0."Id", n."Id", n0."Id", a1."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a1 ON r."RequiredAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a1."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a1."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a2."Id", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a2 - LEFT JOIN "NestedAssociateType" AS n3 ON a2."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a2."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a2."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -WHERE ( - SELECT count(*)::int - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId") = 2 -ORDER BY r."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a1."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Where() - { - await base.Where(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a0."Id", n."Id", n0."Id", a1."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a1 ON r."RequiredAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a1."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a1."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a2."Id", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a2 - LEFT JOIN "NestedAssociateType" AS n3 ON a2."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a2."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a2."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -WHERE ( - SELECT count(*)::int - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" AND a."Int" <> 8) = 2 -ORDER BY r."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a1."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task OrderBy_ElementAt() - { - await base.OrderBy_ElementAt(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a0."Id", n."Id", n0."Id", a1."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a1 ON r."RequiredAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a1."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a1."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a2."Id", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a2 - LEFT JOIN "NestedAssociateType" AS n3 ON a2."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a2."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a2."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -WHERE ( - SELECT a."Int" - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" - ORDER BY a."Id" NULLS FIRST - LIMIT 1 OFFSET 0) = 8 -ORDER BY r."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a1."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - #region Distinct - - public override async Task Distinct() - { - await base.Distinct(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a1."Id", n."Id", n0."Id", a2."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a1 ON r."OptionalAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n ON a1."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a1."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a2 ON r."RequiredAssociateId" = a2."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a2."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a2."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a3."Id", a3."CollectionRootId", a3."Int", a3."Ints", a3."Name", a3."OptionalNestedAssociateId", a3."RequiredNestedAssociateId", a3."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a3 - LEFT JOIN "NestedAssociateType" AS n3 ON a3."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a3."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a3."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a1."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a2."Id" = n7."CollectionAssociateId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT DISTINCT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String" - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" - ) AS a0) = 2 -ORDER BY r."Id" NULLS FIRST, a1."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a2."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Distinct_projected(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT a0."Id", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n."Id" AS "Id0", n0."Id" AS "Id1", n1."Id" AS "Id2", n1."CollectionAssociateId", n1."Int" AS "Int0", n1."Ints" AS "Ints0", n1."Name" AS "Name0", n1."String" AS "String0", n."CollectionAssociateId" AS "CollectionAssociateId0", n."Int" AS "Int1", n."Ints" AS "Ints1", n."Name" AS "Name1", n."String" AS "String1", n0."CollectionAssociateId" AS "CollectionAssociateId1", n0."Int" AS "Int2", n0."Ints" AS "Ints2", n0."Name" AS "Name2", n0."String" AS "String2" - FROM ( - SELECT DISTINCT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String" - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" - ) AS a0 - LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" - INNER JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" - LEFT JOIN "NestedAssociateType" AS n1 ON a0."Id" = n1."CollectionAssociateId" -) AS s ON TRUE -ORDER BY r."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST -"""); - } - - public override async Task Distinct_over_projected_nested_collection() - { - await base.Distinct_over_projected_nested_collection(); - - AssertSql(); - } - - public override async Task Distinct_over_projected_filtered_nested_collection() - { - await base.Distinct_over_projected_filtered_nested_collection(); - - AssertSql(); - } - - #endregion Distinct - - #region Index - - public override async Task Index_constant() - { - await base.Index_constant(); - - AssertSql(); - } - - public override async Task Index_parameter() - { - await base.Index_parameter(); - - AssertSql(); - } - - public override async Task Index_column() - { - await base.Index_column(); - - AssertSql(); - } - - public override async Task Index_out_of_bounds() - { - await base.Index_out_of_bounds(); - - AssertSql(); - } - - #endregion Index - - #region GroupBy - - [ConditionalFact] - public override async Task GroupBy() - { - await base.GroupBy(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a0."Id", n."Id", n0."Id", a1."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a1 ON r."RequiredAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a1."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a1."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a2."Id", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a2 - LEFT JOIN "NestedAssociateType" AS n3 ON a2."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a2."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a2."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -WHERE 16 IN ( - SELECT COALESCE(sum(a."Int"), 0)::int - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" - GROUP BY a."String" -) -ORDER BY r."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a1."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - #endregion GroupBy - - public override async Task Select_within_Select_within_Select_with_aggregates() - { - await base.Select_within_Select_within_Select_with_aggregates(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT max(n."Int") - FROM "NestedAssociateType" AS n - WHERE a."Id" = n."CollectionAssociateId")), 0)::int - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId") -FROM "RootEntity" AS r -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsIncludeNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsIncludeNpgsqlTest.cs deleted file mode 100644 index 07bfae14a4..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsIncludeNpgsqlTest.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsIncludeNpgsqlTest(NavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : NavigationsIncludeRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Include_required(bool async) - { - await base.Include_required(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_optional(bool async) - { - await base.Include_optional(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_collection(bool async) - { - await base.Include_collection(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_required_optional_and_collection(bool async) - { - await base.Include_required_optional_and_collection(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_nested(bool async) - { - await base.Include_nested(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_nested_optional(bool async) - { - await base.Include_nested_optional(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_nested_collection(bool async) - { - await base.Include_nested_collection(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_nested_collection_on_optional(bool async) - { - await base.Include_nested_collection_on_optional(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Include_nested_collection_on_collection(bool async) - { - await base.Include_nested_collection_on_collection(async); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsMiscellaneousNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsMiscellaneousNpgsqlTest.cs deleted file mode 100644 index e0de332894..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsMiscellaneousNpgsqlTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsMiscellaneousNpgsqlTest( - NavigationsNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : NavigationsMiscellaneousRelationalTestBase(fixture, testOutputHelper) -{ - #region Simple filters - - public override async Task Where_on_associate_scalar_property() - { - await base.Where_on_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE a."Int" = 8 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Where_on_optional_associate_scalar_property() - { - await base.Where_on_optional_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -WHERE a."Int" = 8 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Where_on_nested_associate_scalar_property() - { - await base.Where_on_nested_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", a0."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE n."Int" = 8 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - #endregion Simple filters - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsNpgsqlFixture.cs deleted file mode 100644 index 604fad94c0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsNpgsqlFixture : NavigationsRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsPrimitiveCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsPrimitiveCollectionNpgsqlTest.cs deleted file mode 100644 index 0421865be7..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsPrimitiveCollectionNpgsqlTest.cs +++ /dev/null @@ -1,166 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsPrimitiveCollectionNpgsqlTest(NavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : NavigationsPrimitiveCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE cardinality(a."Ints") = 3 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE a."Ints"[1] = 1 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Contains() - { - await base.Contains(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE 3 = ANY (a."Ints") -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Any_predicate() - { - await base.Any_predicate(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE 2 = ANY (a."Ints") -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Nested_Count() - { - await base.Nested_Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", a0."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE cardinality(n."Ints") = 3 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Select_Sum() - { - await base.Select_Sum(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(i0.value), 0)::int - FROM unnest(a."Ints") AS i0(value)) -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -WHERE ( - SELECT COALESCE(sum(i.value), 0)::int - FROM unnest(a."Ints") AS i(value)) >= 6 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs deleted file mode 100644 index c3252acd00..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsProjectionNpgsqlTest.cs +++ /dev/null @@ -1,399 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsProjectionNpgsqlTest(NavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : NavigationsProjectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - #region Scalar properties - - public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -"""); - } - - public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_property_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -"""); - } - - public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_value_type_property_on_null_associate_throws(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Int" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -"""); - } - - public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_property_on_null_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Int" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -"""); - } - - #endregion Scalar properties - - #region Structural properties - - public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n."Id", n0."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST -"""); - } - - public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n."Id", n0."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST -"""); - } - - public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -"""); - } - - public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -"""); - } - - public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -"""); - } - - public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -"""); - } - - public override async Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_associate_via_optional_navigation(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", r0."Id", n."Id", n0."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String" -FROM "RootReferencingEntity" AS r -LEFT JOIN "RootEntity" AS r0 ON r."RootEntityId" = r0."Id" -LEFT JOIN "AssociateType" AS a ON r0."RequiredAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, r0."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST -"""); - } - - public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n."Id", n0."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST -"""); - } - - public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Int" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -"""); - } - - #endregion Structural properties - - #region Structural collection properties - - public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate_collection(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n."Id" AS "Id0", n0."Id" AS "Id1", n1."Id" AS "Id2", n1."CollectionAssociateId", n1."Int" AS "Int0", n1."Ints" AS "Ints0", n1."Name" AS "Name0", n1."String" AS "String0", n."CollectionAssociateId" AS "CollectionAssociateId0", n."Int" AS "Int1", n."Ints" AS "Ints1", n."Name" AS "Name1", n."String" AS "String1", n0."CollectionAssociateId" AS "CollectionAssociateId1", n0."Int" AS "Int2", n0."Ints" AS "Ints2", n0."Name" AS "Name2", n0."String" AS "String2" - FROM "AssociateType" AS a - LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" - INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" - LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -ORDER BY r."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST -"""); - } - - public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", a."Id", n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."Id" = n."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST -"""); - } - - public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", a."Id", n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."Id" = n."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST -"""); - } - - public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_associate_collection(queryTrackingBehavior); - - AssertSql( - """ -SELECT a."Id", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", r."Id", n."Id", n0."Id", n1."Id", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."Id" = a."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -INNER JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."Id" = n1."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST -"""); - } - - public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."Id" = n."CollectionAssociateId" -"""); - } - - public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."Id" = n."CollectionAssociateId" -"""); - } - - #endregion Structural collection properties - - #region Multiple - - public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_duplicated(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", s0."Id", s0."CollectionRootId", s0."Int", s0."Ints", s0."Name", s0."OptionalNestedAssociateId", s0."RequiredNestedAssociateId", s0."String", s0."Id0", s0."Id1", s0."Id2", s0."CollectionAssociateId", s0."Int0", s0."Ints0", s0."Name0", s0."String0", s0."CollectionAssociateId0", s0."Int1", s0."Ints1", s0."Name1", s0."String1", s0."CollectionAssociateId1", s0."Int2", s0."Ints2", s0."Name2", s0."String2", n11."Id", n11."CollectionAssociateId", n11."Int", n11."Ints", n11."Name", n11."String", n12."Id", n12."CollectionAssociateId", n12."Int", n12."Ints", n12."Name", n12."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -LEFT JOIN ( - SELECT a2."Id", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n8."Id" AS "Id0", n9."Id" AS "Id1", n10."Id" AS "Id2", n10."CollectionAssociateId", n10."Int" AS "Int0", n10."Ints" AS "Ints0", n10."Name" AS "Name0", n10."String" AS "String0", n8."CollectionAssociateId" AS "CollectionAssociateId0", n8."Int" AS "Int1", n8."Ints" AS "Ints1", n8."Name" AS "Name1", n8."String" AS "String1", n9."CollectionAssociateId" AS "CollectionAssociateId1", n9."Int" AS "Int2", n9."Ints" AS "Ints2", n9."Name" AS "Name2", n9."String" AS "String2" - FROM "AssociateType" AS a2 - LEFT JOIN "NestedAssociateType" AS n8 ON a2."OptionalNestedAssociateId" = n8."Id" - INNER JOIN "NestedAssociateType" AS n9 ON a2."RequiredNestedAssociateId" = n9."Id" - LEFT JOIN "NestedAssociateType" AS n10 ON a2."Id" = n10."CollectionAssociateId" -) AS s0 ON r."Id" = s0."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n11 ON a."Id" = n11."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n12 ON a0."Id" = n12."CollectionAssociateId" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST, n7."Id" NULLS FIRST, s0."Id" NULLS FIRST, s0."Id0" NULLS FIRST, s0."Id1" NULLS FIRST, s0."Id2" NULLS FIRST, n11."Id" NULLS FIRST -"""); - } - - #endregion Multiple - - #region Subquery - - public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior); - - AssertSql( - """ -SELECT s."Id", s."CollectionAssociateId", s."Int", s."Ints", s."Name", s."String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" - FROM "RootEntity" AS r0 - INNER JOIN "AssociateType" AS a ON r0."RequiredAssociateId" = a."Id" - INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS s ON TRUE -"""); - } - - public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior); - - AssertSql( - """ -SELECT s."Id", s."CollectionAssociateId", s."Int", s."Ints", s."Name", s."String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT n."Id", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" - FROM "RootEntity" AS r0 - LEFT JOIN "AssociateType" AS a ON r0."OptionalAssociateId" = a."Id" - LEFT JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS s ON TRUE -"""); - } - - #endregion Subquery - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsSetOperationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsSetOperationsNpgsqlTest.cs deleted file mode 100644 index 0801f4860b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsSetOperationsNpgsqlTest.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsSetOperationsNpgsqlTest( - NavigationsNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : NavigationsSetOperationsRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Over_associate_collections() - { - await base.Over_associate_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a1."Id", n."Id", n0."Id", a2."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a1 ON r."OptionalAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n ON a1."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a1."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a2 ON r."RequiredAssociateId" = a2."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a2."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a2."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a3."Id", a3."CollectionRootId", a3."Int", a3."Ints", a3."Name", a3."OptionalNestedAssociateId", a3."RequiredNestedAssociateId", a3."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a3 - LEFT JOIN "NestedAssociateType" AS n3 ON a3."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a3."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a3."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a1."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a2."Id" = n7."CollectionAssociateId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" AND a."Int" = 8 - UNION ALL - SELECT 1 - FROM "AssociateType" AS a0 - WHERE r."Id" = a0."CollectionRootId" AND a0."String" = 'foo' - ) AS u) = 4 -ORDER BY r."Id" NULLS FIRST, a1."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a2."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Over_associate_collection_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Over_associate_collection_projected(queryTrackingBehavior); - - AssertSql(); - } - - public override async Task Over_assocate_collection_Select_nested_with_aggregates_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Over_assocate_collection_Select_nested_with_aggregates_projected(queryTrackingBehavior); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT COALESCE(sum(n."Int"), 0)::int - FROM "NestedAssociateType" AS n - WHERE u."Id" = n."CollectionAssociateId")), 0)::int - FROM ( - SELECT a."Id" - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" AND a."Int" = 8 - UNION ALL - SELECT a0."Id" - FROM "AssociateType" AS a0 - WHERE r."Id" = a0."CollectionRootId" AND a0."String" = 'foo' - ) AS u) -FROM "RootEntity" AS r -"""); - } - - public override async Task Over_nested_associate_collection() - { - await base.Over_nested_associate_collection(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n1."Id", n2."Id", n3."Id", n4."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n8."Id", n8."CollectionAssociateId", n8."Int", n8."Ints", n8."Name", n8."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n9."Id", n9."CollectionAssociateId", n9."Int", n9."Ints", n9."Name", n9."String", n3."CollectionAssociateId", n3."Int", n3."Ints", n3."Name", n3."String", n4."CollectionAssociateId", n4."Int", n4."Ints", n4."Name", n4."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN "NestedAssociateType" AS n3 ON a."OptionalNestedAssociateId" = n3."Id" -INNER JOIN "NestedAssociateType" AS n4 ON a."RequiredNestedAssociateId" = n4."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n5."Id" AS "Id0", n6."Id" AS "Id1", n7."Id" AS "Id2", n7."CollectionAssociateId", n7."Int" AS "Int0", n7."Ints" AS "Ints0", n7."Name" AS "Name0", n7."String" AS "String0", n5."CollectionAssociateId" AS "CollectionAssociateId0", n5."Int" AS "Int1", n5."Ints" AS "Ints1", n5."Name" AS "Name1", n5."String" AS "String1", n6."CollectionAssociateId" AS "CollectionAssociateId1", n6."Int" AS "Int2", n6."Ints" AS "Ints2", n6."Name" AS "Name2", n6."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n5 ON a1."OptionalNestedAssociateId" = n5."Id" - INNER JOIN "NestedAssociateType" AS n6 ON a1."RequiredNestedAssociateId" = n6."Id" - LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n8 ON a0."Id" = n8."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n9 ON a."Id" = n9."CollectionAssociateId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM "NestedAssociateType" AS n - WHERE a."Id" = n."CollectionAssociateId" AND n."Int" = 8 - UNION ALL - SELECT 1 - FROM "NestedAssociateType" AS n0 - WHERE a."Id" = n0."CollectionAssociateId" AND n0."String" = 'foo' - ) AS u) = 4 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, n3."Id" NULLS FIRST, n4."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n8."Id" NULLS FIRST -"""); - } - - public override async Task Over_different_collection_properties() - { - await base.Over_different_collection_properties(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n1."Id", n2."Id", n3."Id", n4."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n8."Id", n8."CollectionAssociateId", n8."Int", n8."Ints", n8."Name", n8."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n9."Id", n9."CollectionAssociateId", n9."Int", n9."Ints", n9."Name", n9."String", n3."CollectionAssociateId", n3."Int", n3."Ints", n3."Name", n3."String", n4."CollectionAssociateId", n4."Int", n4."Ints", n4."Name", n4."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN "NestedAssociateType" AS n3 ON a."OptionalNestedAssociateId" = n3."Id" -INNER JOIN "NestedAssociateType" AS n4 ON a."RequiredNestedAssociateId" = n4."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n5."Id" AS "Id0", n6."Id" AS "Id1", n7."Id" AS "Id2", n7."CollectionAssociateId", n7."Int" AS "Int0", n7."Ints" AS "Ints0", n7."Name" AS "Name0", n7."String" AS "String0", n5."CollectionAssociateId" AS "CollectionAssociateId0", n5."Int" AS "Int1", n5."Ints" AS "Ints1", n5."Name" AS "Name1", n5."String" AS "String1", n6."CollectionAssociateId" AS "CollectionAssociateId1", n6."Int" AS "Int2", n6."Ints" AS "Ints2", n6."Name" AS "Name2", n6."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n5 ON a1."OptionalNestedAssociateId" = n5."Id" - INNER JOIN "NestedAssociateType" AS n6 ON a1."RequiredNestedAssociateId" = n6."Id" - LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n8 ON a0."Id" = n8."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n9 ON a."Id" = n9."CollectionAssociateId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM "NestedAssociateType" AS n - WHERE a."Id" = n."CollectionAssociateId" - UNION ALL - SELECT 1 - FROM "NestedAssociateType" AS n0 - WHERE a0."Id" IS NOT NULL AND a0."Id" = n0."CollectionAssociateId" - ) AS u) = 4 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, n3."Id" NULLS FIRST, n4."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n8."Id" NULLS FIRST -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualityNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualityNpgsqlTest.cs deleted file mode 100644 index 54f9336301..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/Navigations/NavigationsStructuralEqualityNpgsqlTest.cs +++ /dev/null @@ -1,418 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.Navigations; - -public class NavigationsStructuralEqualityNpgsqlTest( - NavigationsNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : NavigationsStructuralEqualityRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Two_associates() - { - await base.Two_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE a."Id" = a0."Id" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Two_nested_associates() - { - await base.Two_nested_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", a0."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE n."Id" = n0."Id" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Not_equals() - { - await base.Not_equals(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE a."Id" <> a0."Id" OR a0."Id" IS NULL -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -WHERE a."Id" IS NULL -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Associate_with_parameter_null() - { - await base.Associate_with_parameter_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", n0."Id", a0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a ON r."OptionalAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a0 ON r."RequiredAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a0."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -WHERE a."Id" IS NULL -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_inline_null() - { - await base.Nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", a0."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "NestedAssociateType" AS n ON a."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE n."Id" IS NULL -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_inline() - { - await base.Nested_associate_with_inline(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", a0."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE n."Id" = 1000 -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_parameter() - { - await base.Nested_associate_with_parameter(); - - AssertSql( - """ -@entity_equality_nested_Id='1000' (Nullable = true) - -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", n."Id", a0."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -INNER JOIN "NestedAssociateType" AS n ON a."RequiredNestedAssociateId" = n."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE n."Id" = @entity_equality_nested_Id -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, n."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Two_nested_collections() - { - await base.Two_nested_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n."Id", n0."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n3 ON a1."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a1."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a1."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a."Id" = n7."CollectionAssociateId" -WHERE a."Id" = a0."Id" -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - public override async Task Nested_collection_with_inline() - { - await base.Nested_collection_with_inline(); - - AssertSql(); - } - - public override async Task Nested_collection_with_parameter() - { - await base.Nested_collection_with_parameter(); - - AssertSql(); - } - - #region Contains - - public override async Task Contains_with_inline() - { - await base.Contains_with_inline(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n0."Id", n1."Id", n2."Id", n3."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n8."Id", n8."CollectionAssociateId", n8."Int", n8."Ints", n8."Name", n8."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n3."CollectionAssociateId", n3."Int", n3."Ints", n3."Name", n3."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -INNER JOIN "NestedAssociateType" AS n3 ON a."RequiredNestedAssociateId" = n3."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n4."Id" AS "Id0", n5."Id" AS "Id1", n6."Id" AS "Id2", n6."CollectionAssociateId", n6."Int" AS "Int0", n6."Ints" AS "Ints0", n6."Name" AS "Name0", n6."String" AS "String0", n4."CollectionAssociateId" AS "CollectionAssociateId0", n4."Int" AS "Int1", n4."Ints" AS "Ints1", n4."Name" AS "Name1", n4."String" AS "String1", n5."CollectionAssociateId" AS "CollectionAssociateId1", n5."Int" AS "Int2", n5."Ints" AS "Ints2", n5."Name" AS "Name2", n5."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n4 ON a1."OptionalNestedAssociateId" = n4."Id" - INNER JOIN "NestedAssociateType" AS n5 ON a1."RequiredNestedAssociateId" = n5."Id" - LEFT JOIN "NestedAssociateType" AS n6 ON a1."Id" = n6."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n8 ON a."Id" = n8."CollectionAssociateId" -WHERE EXISTS ( - SELECT 1 - FROM "NestedAssociateType" AS n - WHERE a."Id" = n."CollectionAssociateId" AND n."Id" = 1002) -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, n3."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n7."Id" NULLS FIRST -"""); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - AssertSql( - """ -@entity_equality_nested_Id='1002' (Nullable = true) - -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n0."Id", n1."Id", n2."Id", n3."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n8."Id", n8."CollectionAssociateId", n8."Int", n8."Ints", n8."Name", n8."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n3."CollectionAssociateId", n3."Int", n3."Ints", n3."Name", n3."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -INNER JOIN "NestedAssociateType" AS n3 ON a."RequiredNestedAssociateId" = n3."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n4."Id" AS "Id0", n5."Id" AS "Id1", n6."Id" AS "Id2", n6."CollectionAssociateId", n6."Int" AS "Int0", n6."Ints" AS "Ints0", n6."Name" AS "Name0", n6."String" AS "String0", n4."CollectionAssociateId" AS "CollectionAssociateId0", n4."Int" AS "Int1", n4."Ints" AS "Ints1", n4."Name" AS "Name1", n4."String" AS "String1", n5."CollectionAssociateId" AS "CollectionAssociateId1", n5."Int" AS "Int2", n5."Ints" AS "Ints2", n5."Name" AS "Name2", n5."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n4 ON a1."OptionalNestedAssociateId" = n4."Id" - INNER JOIN "NestedAssociateType" AS n5 ON a1."RequiredNestedAssociateId" = n5."Id" - LEFT JOIN "NestedAssociateType" AS n6 ON a1."Id" = n6."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n8 ON a."Id" = n8."CollectionAssociateId" -WHERE EXISTS ( - SELECT 1 - FROM "NestedAssociateType" AS n - WHERE a."Id" = n."CollectionAssociateId" AND n."Id" = @entity_equality_nested_Id) -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, n3."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n7."Id" NULLS FIRST -"""); - } - - public override async Task Contains_with_operators_composed_on_the_collection() - { - await base.Contains_with_operators_composed_on_the_collection(); - - AssertSql( - """ -@get_Item_Int='106' -@entity_equality_get_Item_Id='3003' (Nullable = true) - -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a."Id", a0."Id", n0."Id", n1."Id", n2."Id", n3."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", a."CollectionRootId", a."Int", a."Ints", a."Name", a."OptionalNestedAssociateId", a."RequiredNestedAssociateId", a."String", n8."Id", n8."CollectionAssociateId", n8."Int", n8."Ints", n8."Name", n8."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String", n3."CollectionAssociateId", n3."Int", n3."Ints", n3."Name", n3."String" -FROM "RootEntity" AS r -INNER JOIN "AssociateType" AS a ON r."RequiredAssociateId" = a."Id" -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."OptionalNestedAssociateId" = n0."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a0."RequiredNestedAssociateId" = n1."Id" -LEFT JOIN "NestedAssociateType" AS n2 ON a."OptionalNestedAssociateId" = n2."Id" -INNER JOIN "NestedAssociateType" AS n3 ON a."RequiredNestedAssociateId" = n3."Id" -LEFT JOIN ( - SELECT a1."Id", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n4."Id" AS "Id0", n5."Id" AS "Id1", n6."Id" AS "Id2", n6."CollectionAssociateId", n6."Int" AS "Int0", n6."Ints" AS "Ints0", n6."Name" AS "Name0", n6."String" AS "String0", n4."CollectionAssociateId" AS "CollectionAssociateId0", n4."Int" AS "Int1", n4."Ints" AS "Ints1", n4."Name" AS "Name1", n4."String" AS "String1", n5."CollectionAssociateId" AS "CollectionAssociateId1", n5."Int" AS "Int2", n5."Ints" AS "Ints2", n5."Name" AS "Name2", n5."String" AS "String2" - FROM "AssociateType" AS a1 - LEFT JOIN "NestedAssociateType" AS n4 ON a1."OptionalNestedAssociateId" = n4."Id" - INNER JOIN "NestedAssociateType" AS n5 ON a1."RequiredNestedAssociateId" = n5."Id" - LEFT JOIN "NestedAssociateType" AS n6 ON a1."Id" = n6."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n7 ON a0."Id" = n7."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n8 ON a."Id" = n8."CollectionAssociateId" -WHERE EXISTS ( - SELECT 1 - FROM "NestedAssociateType" AS n - WHERE a."Id" = n."CollectionAssociateId" AND n."Int" > @get_Item_Int AND n."Id" = @entity_equality_get_Item_Id) -ORDER BY r."Id" NULLS FIRST, a."Id" NULLS FIRST, a0."Id" NULLS FIRST, n0."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, n3."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n7."Id" NULLS FIRST -"""); - } - - public override async Task Contains_with_nested_and_composed_operators() - { - await base.Contains_with_nested_and_composed_operators(); - - AssertSql( - """ -@get_Item_Id='302' -@entity_equality_get_Item_Id='303' (Nullable = true) - -SELECT r."Id", r."Name", r."OptionalAssociateId", r."RequiredAssociateId", a0."Id", n."Id", n0."Id", a1."Id", n1."Id", n2."Id", s."Id", s."CollectionRootId", s."Int", s."Ints", s."Name", s."OptionalNestedAssociateId", s."RequiredNestedAssociateId", s."String", s."Id0", s."Id1", s."Id2", s."CollectionAssociateId", s."Int0", s."Ints0", s."Name0", s."String0", s."CollectionAssociateId0", s."Int1", s."Ints1", s."Name1", s."String1", s."CollectionAssociateId1", s."Int2", s."Ints2", s."Name2", s."String2", a0."CollectionRootId", a0."Int", a0."Ints", a0."Name", a0."OptionalNestedAssociateId", a0."RequiredNestedAssociateId", a0."String", n6."Id", n6."CollectionAssociateId", n6."Int", n6."Ints", n6."Name", n6."String", n."CollectionAssociateId", n."Int", n."Ints", n."Name", n."String", n0."CollectionAssociateId", n0."Int", n0."Ints", n0."Name", n0."String", a1."CollectionRootId", a1."Int", a1."Ints", a1."Name", a1."OptionalNestedAssociateId", a1."RequiredNestedAssociateId", a1."String", n7."Id", n7."CollectionAssociateId", n7."Int", n7."Ints", n7."Name", n7."String", n1."CollectionAssociateId", n1."Int", n1."Ints", n1."Name", n1."String", n2."CollectionAssociateId", n2."Int", n2."Ints", n2."Name", n2."String" -FROM "RootEntity" AS r -LEFT JOIN "AssociateType" AS a0 ON r."OptionalAssociateId" = a0."Id" -LEFT JOIN "NestedAssociateType" AS n ON a0."OptionalNestedAssociateId" = n."Id" -LEFT JOIN "NestedAssociateType" AS n0 ON a0."RequiredNestedAssociateId" = n0."Id" -INNER JOIN "AssociateType" AS a1 ON r."RequiredAssociateId" = a1."Id" -LEFT JOIN "NestedAssociateType" AS n1 ON a1."OptionalNestedAssociateId" = n1."Id" -INNER JOIN "NestedAssociateType" AS n2 ON a1."RequiredNestedAssociateId" = n2."Id" -LEFT JOIN ( - SELECT a2."Id", a2."CollectionRootId", a2."Int", a2."Ints", a2."Name", a2."OptionalNestedAssociateId", a2."RequiredNestedAssociateId", a2."String", n3."Id" AS "Id0", n4."Id" AS "Id1", n5."Id" AS "Id2", n5."CollectionAssociateId", n5."Int" AS "Int0", n5."Ints" AS "Ints0", n5."Name" AS "Name0", n5."String" AS "String0", n3."CollectionAssociateId" AS "CollectionAssociateId0", n3."Int" AS "Int1", n3."Ints" AS "Ints1", n3."Name" AS "Name1", n3."String" AS "String1", n4."CollectionAssociateId" AS "CollectionAssociateId1", n4."Int" AS "Int2", n4."Ints" AS "Ints2", n4."Name" AS "Name2", n4."String" AS "String2" - FROM "AssociateType" AS a2 - LEFT JOIN "NestedAssociateType" AS n3 ON a2."OptionalNestedAssociateId" = n3."Id" - INNER JOIN "NestedAssociateType" AS n4 ON a2."RequiredNestedAssociateId" = n4."Id" - LEFT JOIN "NestedAssociateType" AS n5 ON a2."Id" = n5."CollectionAssociateId" -) AS s ON r."Id" = s."CollectionRootId" -LEFT JOIN "NestedAssociateType" AS n6 ON a0."Id" = n6."CollectionAssociateId" -LEFT JOIN "NestedAssociateType" AS n7 ON a1."Id" = n7."CollectionAssociateId" -WHERE EXISTS ( - SELECT 1 - FROM "AssociateType" AS a - WHERE r."Id" = a."CollectionRootId" AND a."Id" > @get_Item_Id AND a."Id" = @entity_equality_get_Item_Id) -ORDER BY r."Id" NULLS FIRST, a0."Id" NULLS FIRST, n."Id" NULLS FIRST, n0."Id" NULLS FIRST, a1."Id" NULLS FIRST, n1."Id" NULLS FIRST, n2."Id" NULLS FIRST, s."Id" NULLS FIRST, s."Id0" NULLS FIRST, s."Id1" NULLS FIRST, s."Id2" NULLS FIRST, n6."Id" NULLS FIRST -"""); - } - - #endregion Contains - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonBulkUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonBulkUpdateNpgsqlTest.cs deleted file mode 100644 index 37d6ef2432..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonBulkUpdateNpgsqlTest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonBulkUpdateNpgsqlTest( - OwnedJsonNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedJsonBulkUpdateRelationalTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs deleted file mode 100644 index e062849bf7..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionNpgsqlTest.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonCollectionNpgsqlTest(OwnedJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedJsonCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb - )) WITH ORDINALITY AS a) = 2 -"""); - } - - public override async Task Where() - { - await base.Where(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a - WHERE a."Int" <> 8) = 2 -"""); - } - - public override async Task OrderBy_ElementAt() - { - await base.OrderBy_ElementAt(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT a."Int" - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a - ORDER BY a."Id" NULLS FIRST - LIMIT 1 OFFSET 0) = 8 -"""); - } - - #region Distinct - - public override async Task Distinct() - { - await base.Distinct(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE ( - SELECT count(*)::int - FROM ( - SELECT DISTINCT r."Id", a."Id" AS "Id0", a."Int", a."Ints", a."Name", a."String", a."NestedCollection" AS c, a."OptionalNestedAssociate" AS c0, a."RequiredNestedAssociate" AS c1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb - )) WITH ORDINALITY AS a - ) AS a0) = 2 -"""); - } - - public override async Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Distinct_projected(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", a0."Id", a0."Id0", a0."Int", a0."Ints", a0."Name", a0."String", a0.c, a0.c0, a0.c1 -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT DISTINCT r."Id", a."Id" AS "Id0", a."Int", a."Ints", a."Name", a."String", a."NestedCollection" AS c, a."OptionalNestedAssociate" AS c0, a."RequiredNestedAssociate" AS c1 - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb - )) WITH ORDINALITY AS a -) AS a0 ON TRUE -ORDER BY r."Id" NULLS FIRST, a0."Id0" NULLS FIRST, a0."Int" NULLS FIRST, a0."Ints" NULLS FIRST, a0."Name" NULLS FIRST -"""); - } - } - - public override async Task Distinct_over_projected_nested_collection() - { - await base.Distinct_over_projected_nested_collection(); - - AssertSql(); - } - - public override async Task Distinct_over_projected_filtered_nested_collection() - { - await base.Distinct_over_projected_filtered_nested_collection(); - - AssertSql(); - } - - #endregion Distinct - - #region Index - - public override async Task Index_constant() - { - await base.Index_constant(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> '{0,Int}' AS integer)) = 8 -"""); - } - - public override async Task Index_parameter() - { - await base.Index_parameter(); - - AssertSql( - """ -@i='0' - -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> ARRAY[@i,'Int']::text[] AS integer)) = 8 -"""); - } - - public override async Task Index_column() - { - await base.Index_column(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> ARRAY[r."Id" - 1,'Int']::text[] AS integer)) = 8 -"""); - } - - public override async Task Index_out_of_bounds() - { - await base.Index_out_of_bounds(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."AssociateCollection" #>> '{9999,Int}' AS integer)) = 8 -"""); - } - - #endregion Index - - #region GroupBy - - [ConditionalFact] - public override async Task GroupBy() - { - await base.GroupBy(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE 16 IN ( - SELECT COALESCE(sum(a0."Int"), 0)::int - FROM ( - SELECT a."Id" AS "Id0", a."Int", a."Ints", a."Name", a."String", a."String" AS "Key" - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS a - ) AS a0 - GROUP BY a0."Key" -) -"""); - } - - #endregion GroupBy - - public override async Task Select_within_Select_within_Select_with_aggregates() - { - await base.Select_within_Select_within_Select_with_aggregates(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT max(n."Int") - FROM ROWS FROM (jsonb_to_recordset(a."NestedCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text - )) WITH ORDINALITY AS n)), 0)::int - FROM ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ("NestedCollection" jsonb)) WITH ORDINALITY AS a) -FROM "RootEntity" AS r -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonMiscellaneousNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonMiscellaneousNpgsqlTest.cs deleted file mode 100644 index 72ba854d48..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonMiscellaneousNpgsqlTest.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonMiscellaneousNpgsqlTest( - OwnedJsonNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedJsonMiscellaneousRelationalTestBase(fixture, testOutputHelper) -{ - #region Simple filters - - public override async Task Where_on_associate_scalar_property() - { - await base.Where_on_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."RequiredAssociate" ->> 'Int' AS integer)) = 8 -"""); - } - - public override async Task Where_on_optional_associate_scalar_property() - { - await base.Where_on_optional_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."OptionalAssociate" ->> 'Int' AS integer)) = 8 -"""); - } - - public override async Task Where_on_nested_associate_scalar_property() - { - await base.Where_on_nested_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."RequiredAssociate" #>> '{RequiredNestedAssociate,Int}' AS integer)) = 8 -"""); - } - - #endregion Simple filters - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonNpgsqlFixture.cs deleted file mode 100644 index 0aaf845cfc..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonNpgsqlFixture : OwnedJsonRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionNpgsqlTest.cs deleted file mode 100644 index 1ed4990af6..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonPrimitiveCollectionNpgsqlTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonPrimitiveCollectionNpgsqlTest(OwnedJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedJsonPrimitiveCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE jsonb_array_length(r."RequiredAssociate" -> 'Ints') = 3 -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (CAST(r."RequiredAssociate" #>> '{Ints,0}' AS integer)) = 1 -"""); - } - - public override async Task Contains() - { - await base.Contains(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'Ints') @> to_jsonb(3) -"""); - } - - public override async Task Any_predicate() - { - await base.Any_predicate(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" -> 'Ints') @> to_jsonb(2) -"""); - } - - public override async Task Nested_Count() - { - await base.Nested_Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE jsonb_array_length(r."RequiredAssociate" #> '{RequiredNestedAssociate,Ints}') = 3 -"""); - } - - public override async Task Select_Sum() - { - await base.Select_Sum(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(i0.element::int), 0)::int - FROM jsonb_array_elements_text(r."RequiredAssociate" -> 'Ints') WITH ORDINALITY AS i0(element)) -FROM "RootEntity" AS r -WHERE ( - SELECT COALESCE(sum(i.element::int), 0)::int - FROM jsonb_array_elements_text(r."RequiredAssociate" -> 'Ints') WITH ORDINALITY AS i(element)) >= 6 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs deleted file mode 100644 index 26200c5a46..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionNpgsqlTest.cs +++ /dev/null @@ -1,373 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonProjectionNpgsqlTest(OwnedJsonNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedJsonProjectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -"""); - } - - #region Scalar properties - - public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate" ->> 'String' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_property_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate" ->> 'String' -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_value_type_property_on_null_associate_throws(queryTrackingBehavior); - - AssertSql( - """ -SELECT CAST(r."OptionalAssociate" ->> 'Int' AS integer) -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_property_on_null_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT CAST(r."OptionalAssociate" ->> 'Int' AS integer) -FROM "RootEntity" AS r -"""); - } - - #endregion Scalar properties - - #region Structural properties - - public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."RequiredAssociate", r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."OptionalAssociate", r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."RequiredAssociate" -> 'RequiredNestedAssociate', r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."RequiredAssociate" -> 'OptionalNestedAssociate', r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."OptionalAssociate" -> 'RequiredNestedAssociate', r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."OptionalAssociate" -> 'OptionalNestedAssociate', r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_associate_via_optional_navigation(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."RequiredAssociate", r0."Id" -FROM "RootReferencingEntity" AS r -LEFT JOIN "RootEntity" AS r0 ON r."RootEntityId" = r0."Id" -"""); - } - } - - public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."RequiredAssociate", r."Id" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT CAST(r."RequiredAssociate" ->> 'Int' AS integer) -FROM "RootEntity" AS r -"""); - } - - #endregion Structural properties - - #region Collection - - public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate_collection(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."AssociateCollection", r."Id" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - } - - public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."RequiredAssociate" -> 'NestedCollection', r."Id" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - } - - public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."OptionalAssociate" -> 'NestedCollection', r."Id" -FROM "RootEntity" AS r -ORDER BY r."Id" NULLS FIRST -"""); - } - } - - public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_associate_collection(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", a."Id", a."Int", a."Ints", a."Name", a."String", a."NestedCollection", a."OptionalNestedAssociate", a."RequiredNestedAssociate" -FROM "RootEntity" AS r -JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."AssociateCollection") AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text, - "NestedCollection" jsonb, - "OptionalNestedAssociate" jsonb, - "RequiredNestedAssociate" jsonb -)) WITH ORDINALITY AS a ON TRUE -"""); - } - } - - public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", n."Id", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."RequiredAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text -)) WITH ORDINALITY AS n ON TRUE -"""); - } - } - - public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", n."Id", n."Int", n."Ints", n."Name", n."String" -FROM "RootEntity" AS r -JOIN LATERAL ROWS FROM (jsonb_to_recordset(r."OptionalAssociate" -> 'NestedCollection') AS ( - "Id" integer, - "Int" integer, - "Ints" jsonb, - "Name" text, - "String" text -)) WITH ORDINALITY AS n ON TRUE -"""); - } - } - - #endregion Collection - - #region Multiple - - public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_duplicated(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -"""); - } - - #endregion Multiple - - #region Subquery - - public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1.c, r1."Id" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."RequiredAssociate" -> 'RequiredNestedAssociate' AS c, r0."Id" - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - } - - public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1.c, r1."Id" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."OptionalAssociate" -> 'RequiredNestedAssociate' AS c, r0."Id" - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - } - - #endregion Subquery - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityNpgsqlTest.cs deleted file mode 100644 index 0889de0fad..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityNpgsqlTest.cs +++ /dev/null @@ -1,160 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonStructuralEqualityNpgsqlTest( - OwnedJsonNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedJsonStructuralEqualityRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Two_associates() - { - await base.Two_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE FALSE -"""); - } - - public override async Task Two_nested_associates() - { - await base.Two_nested_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE FALSE -"""); - } - - public override async Task Not_equals() - { - await base.Not_equals(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE FALSE -"""); - } - - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."OptionalAssociate") IS NULL -"""); - } - - public override async Task Associate_with_parameter_null() - { - await base.Associate_with_parameter_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE FALSE -"""); - } - - public override async Task Nested_associate_with_inline_null() - { - await base.Nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE (r."RequiredAssociate" ->> 'OptionalNestedAssociate') IS NULL -"""); - } - - public override async Task Nested_associate_with_inline() - { - await base.Nested_associate_with_inline(); - - AssertSql( -); - } - - public override async Task Nested_associate_with_parameter() - { - await base.Nested_associate_with_parameter(); - - AssertSql( -); - } - - public override async Task Two_nested_collections() - { - await base.Two_nested_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", r."AssociateCollection", r."OptionalAssociate", r."RequiredAssociate" -FROM "RootEntity" AS r -WHERE FALSE -"""); - } - - public override async Task Nested_collection_with_inline() - { - await base.Nested_collection_with_inline(); - - AssertSql(); - } - - public override async Task Nested_collection_with_parameter() - { - await base.Nested_collection_with_parameter(); - - AssertSql(); - } - - #region Contains - - public override async Task Contains_with_inline() - { - await base.Contains_with_inline(); - - AssertSql(); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - AssertSql(); - } - - public override async Task Contains_with_operators_composed_on_the_collection() - { - await base.Contains_with_operators_composed_on_the_collection(); - - AssertSql(); - } - - public override async Task Contains_with_nested_and_composed_operators() - { - await base.Contains_with_nested_and_composed_operators(); - - AssertSql(); - } - - #endregion Contains - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonTypeNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonTypeNpgsqlFixture.cs deleted file mode 100644 index 118d582edb..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonTypeNpgsqlFixture.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson; - -public class OwnedJsonTypeNpgsqlFixture : OwnedJsonRelationalFixtureBase -{ - protected override string StoreName - => "OwnedJsonTypeRelationshipsQueryTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - // protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - // { - // base.OnModelCreating(modelBuilder, context); - - // modelBuilder.Entity().OwnsOne(x => x.RequiredTrunk).HasColumnType("json"); - // modelBuilder.Entity().OwnsOne(x => x.OptionalTrunk).HasColumnType("json"); - // modelBuilder.Entity().OwnsMany(x => x.CollectionTrunk).HasColumnType("json"); - // } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionNpgsqlTest.cs deleted file mode 100644 index ceda85ec44..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionNpgsqlTest.cs +++ /dev/null @@ -1,272 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsCollectionNpgsqlTest(OwnedNavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedNavigationsCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."RootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r8."AssociateTypeRootEntityId", r8."Id", r8."Int", r8."Ints", r8."Name", r8."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r1 ON r."Id" = r1."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r1."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r3 ON r1."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r4."RootEntityId", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String", r5."AssociateTypeRootEntityId", r5."AssociateTypeId", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r6."AssociateTypeId" AS "AssociateTypeId0", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r7."AssociateTypeId" AS "AssociateTypeId1", r7."Id" AS "Id0", r7."Int" AS "Int0", r7."Ints" AS "Ints0", r7."Name" AS "Name0", r7."String" AS "String0", r5."Id" AS "Id1", r5."Int" AS "Int1", r5."Ints" AS "Ints1", r5."Name" AS "Name1", r5."String" AS "String1", r6."Id" AS "Id2", r6."Int" AS "Int2", r6."Ints" AS "Ints2", r6."Name" AS "Name2", r6."String" AS "String2" - FROM "RelatedCollection" AS r4 - LEFT JOIN "RelatedCollection_OptionalNested" AS r5 ON r4."RootEntityId" = r5."AssociateTypeRootEntityId" AND r4."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r6 ON r4."RootEntityId" = r6."AssociateTypeRootEntityId" AND r4."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r7 ON r4."RootEntityId" = r7."AssociateTypeRootEntityId" AND r4."Id" = r7."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r8 ON r1."RootEntityId" = r8."AssociateTypeRootEntityId" -WHERE ( - SELECT count(*)::int - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId") = 2 -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."RootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r8."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Where() - { - await base.Where(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."RootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r8."AssociateTypeRootEntityId", r8."Id", r8."Int", r8."Ints", r8."Name", r8."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r1 ON r."Id" = r1."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r1."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r3 ON r1."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r4."RootEntityId", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String", r5."AssociateTypeRootEntityId", r5."AssociateTypeId", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r6."AssociateTypeId" AS "AssociateTypeId0", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r7."AssociateTypeId" AS "AssociateTypeId1", r7."Id" AS "Id0", r7."Int" AS "Int0", r7."Ints" AS "Ints0", r7."Name" AS "Name0", r7."String" AS "String0", r5."Id" AS "Id1", r5."Int" AS "Int1", r5."Ints" AS "Ints1", r5."Name" AS "Name1", r5."String" AS "String1", r6."Id" AS "Id2", r6."Int" AS "Int2", r6."Ints" AS "Ints2", r6."Name" AS "Name2", r6."String" AS "String2" - FROM "RelatedCollection" AS r4 - LEFT JOIN "RelatedCollection_OptionalNested" AS r5 ON r4."RootEntityId" = r5."AssociateTypeRootEntityId" AND r4."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r6 ON r4."RootEntityId" = r6."AssociateTypeRootEntityId" AND r4."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r7 ON r4."RootEntityId" = r7."AssociateTypeRootEntityId" AND r4."Id" = r7."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r8 ON r1."RootEntityId" = r8."AssociateTypeRootEntityId" -WHERE ( - SELECT count(*)::int - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId" AND r0."Int" <> 8) = 2 -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."RootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r8."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task OrderBy_ElementAt() - { - await base.OrderBy_ElementAt(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."RootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r8."AssociateTypeRootEntityId", r8."Id", r8."Int", r8."Ints", r8."Name", r8."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r1 ON r."Id" = r1."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r1."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r3 ON r1."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r4."RootEntityId", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String", r5."AssociateTypeRootEntityId", r5."AssociateTypeId", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r6."AssociateTypeId" AS "AssociateTypeId0", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r7."AssociateTypeId" AS "AssociateTypeId1", r7."Id" AS "Id0", r7."Int" AS "Int0", r7."Ints" AS "Ints0", r7."Name" AS "Name0", r7."String" AS "String0", r5."Id" AS "Id1", r5."Int" AS "Int1", r5."Ints" AS "Ints1", r5."Name" AS "Name1", r5."String" AS "String1", r6."Id" AS "Id2", r6."Int" AS "Int2", r6."Ints" AS "Ints2", r6."Name" AS "Name2", r6."String" AS "String2" - FROM "RelatedCollection" AS r4 - LEFT JOIN "RelatedCollection_OptionalNested" AS r5 ON r4."RootEntityId" = r5."AssociateTypeRootEntityId" AND r4."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r6 ON r4."RootEntityId" = r6."AssociateTypeRootEntityId" AND r4."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r7 ON r4."RootEntityId" = r7."AssociateTypeRootEntityId" AND r4."Id" = r7."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r8 ON r1."RootEntityId" = r8."AssociateTypeRootEntityId" -WHERE ( - SELECT r0."Int" - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId" - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 OFFSET 0) = 8 -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."RootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r8."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #region Distinct - - public override async Task Distinct() - { - await base.Distinct(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r2."RootEntityId", r3."AssociateTypeRootEntityId", r4."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r9."AssociateTypeRootEntityId", r9."Id", r9."Int", r9."Ints", r9."Name", r9."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r2 ON r."Id" = r2."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r3 ON r2."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r4 ON r2."RootEntityId" = r4."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r5."RootEntityId", r5."Id", r5."Int", r5."Ints", r5."Name", r5."String", r6."AssociateTypeRootEntityId", r6."AssociateTypeId", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r7."AssociateTypeId" AS "AssociateTypeId0", r8."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r8."AssociateTypeId" AS "AssociateTypeId1", r8."Id" AS "Id0", r8."Int" AS "Int0", r8."Ints" AS "Ints0", r8."Name" AS "Name0", r8."String" AS "String0", r6."Id" AS "Id1", r6."Int" AS "Int1", r6."Ints" AS "Ints1", r6."Name" AS "Name1", r6."String" AS "String1", r7."Id" AS "Id2", r7."Int" AS "Int2", r7."Ints" AS "Ints2", r7."Name" AS "Name2", r7."String" AS "String2" - FROM "RelatedCollection" AS r5 - LEFT JOIN "RelatedCollection_OptionalNested" AS r6 ON r5."RootEntityId" = r6."AssociateTypeRootEntityId" AND r5."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r7 ON r5."RootEntityId" = r7."AssociateTypeRootEntityId" AND r5."Id" = r7."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r8 ON r5."RootEntityId" = r8."AssociateTypeRootEntityId" AND r5."Id" = r8."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r9 ON r2."RootEntityId" = r9."AssociateTypeRootEntityId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT DISTINCT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String" - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId" - ) AS r1) = 2 -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r2."RootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r4."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r9."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Distinct_projected(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."Id00", s."Int00", s."Ints00", s."Name00", s."String00", s."AssociateTypeRootEntityId00", s."AssociateTypeId00", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r1."RootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r4."Id" AS "Id0", r4."Int" AS "Int0", r4."Ints" AS "Ints0", r4."Name" AS "Name0", r4."String" AS "String0", r1."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r1."AssociateTypeId" AS "AssociateTypeId0", r1."Id0" AS "Id00", r1."Int0" AS "Int00", r1."Ints0" AS "Ints00", r1."Name0" AS "Name00", r1."String0" AS "String00", r1."AssociateTypeRootEntityId0" AS "AssociateTypeRootEntityId00", r1."AssociateTypeId0" AS "AssociateTypeId00", r1."Id1", r1."Int1", r1."Ints1", r1."Name1", r1."String1" - FROM ( - SELECT DISTINCT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r2."AssociateTypeRootEntityId", r2."AssociateTypeId", r2."Id" AS "Id0", r2."Int" AS "Int0", r2."Ints" AS "Ints0", r2."Name" AS "Name0", r2."String" AS "String0", r3."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r3."AssociateTypeId" AS "AssociateTypeId0", r3."Id" AS "Id1", r3."Int" AS "Int1", r3."Ints" AS "Ints1", r3."Name" AS "Name1", r3."String" AS "String1" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_OptionalNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" AND r0."Id" = r2."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" AND r0."Id" = r3."AssociateTypeId" - WHERE r."Id" = r0."RootEntityId" - ) AS r1 - LEFT JOIN "RelatedCollection_NestedCollection" AS r4 ON r1."RootEntityId" = r4."AssociateTypeRootEntityId" AND r1."Id" = r4."AssociateTypeId" -) AS s ON TRUE -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST -"""); - } - } - - public override async Task Distinct_over_projected_nested_collection() - { - await base.Distinct_over_projected_nested_collection(); - - AssertSql(); - } - - public override async Task Distinct_over_projected_filtered_nested_collection() - { - await base.Distinct_over_projected_filtered_nested_collection(); - - AssertSql(); - } - - #endregion Distinct - - #region Index - - public override async Task Index_constant() - { - await base.Index_constant(); - - AssertSql(); - } - - public override async Task Index_parameter() - { - await base.Index_parameter(); - - AssertSql(); - } - - public override async Task Index_column() - { - await base.Index_column(); - - AssertSql(); - } - - public override async Task Index_out_of_bounds() - { - await base.Index_out_of_bounds(); - - AssertSql(); - } - - #endregion Index - - #region GroupBy - - [ConditionalFact] - public override async Task GroupBy() - { - await base.GroupBy(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."RootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r8."AssociateTypeRootEntityId", r8."Id", r8."Int", r8."Ints", r8."Name", r8."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r1 ON r."Id" = r1."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r1."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r3 ON r1."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r4."RootEntityId", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String", r5."AssociateTypeRootEntityId", r5."AssociateTypeId", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r6."AssociateTypeId" AS "AssociateTypeId0", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r7."AssociateTypeId" AS "AssociateTypeId1", r7."Id" AS "Id0", r7."Int" AS "Int0", r7."Ints" AS "Ints0", r7."Name" AS "Name0", r7."String" AS "String0", r5."Id" AS "Id1", r5."Int" AS "Int1", r5."Ints" AS "Ints1", r5."Name" AS "Name1", r5."String" AS "String1", r6."Id" AS "Id2", r6."Int" AS "Int2", r6."Ints" AS "Ints2", r6."Name" AS "Name2", r6."String" AS "String2" - FROM "RelatedCollection" AS r4 - LEFT JOIN "RelatedCollection_OptionalNested" AS r5 ON r4."RootEntityId" = r5."AssociateTypeRootEntityId" AND r4."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r6 ON r4."RootEntityId" = r6."AssociateTypeRootEntityId" AND r4."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r7 ON r4."RootEntityId" = r7."AssociateTypeRootEntityId" AND r4."Id" = r7."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r8 ON r1."RootEntityId" = r8."AssociateTypeRootEntityId" -WHERE 16 IN ( - SELECT COALESCE(sum(r0."Int"), 0)::int - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId" - GROUP BY r0."String" -) -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."RootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r8."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #endregion GroupBy - - public override async Task Select_within_Select_within_Select_with_aggregates() - { - await base.Select_within_Select_within_Select_with_aggregates(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT max(r1."Int") - FROM "RelatedCollection_NestedCollection" AS r1 - WHERE r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId")), 0)::int - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId") -FROM "RootEntity" AS r -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsMiscellaneousNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsMiscellaneousNpgsqlTest.cs deleted file mode 100644 index fa3d39c680..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsMiscellaneousNpgsqlTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsMiscellaneousNpgsqlTest( - OwnedNavigationsNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedNavigationsMiscellaneousRelationalTestBase(fixture, testOutputHelper) -{ - #region Simple filters - - public override async Task Where_on_associate_scalar_property() - { - await base.Where_on_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE r0."Int" = 8 -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Where_on_optional_associate_scalar_property() - { - await base.Where_on_optional_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE o."Int" = 8 -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Where_on_nested_associate_scalar_property() - { - await base.Where_on_nested_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", r1."AssociateTypeRootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE r1."Int" = 8 -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #endregion Simple filters - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsNpgsqlFixture.cs deleted file mode 100644 index 4a589a0f9e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsNpgsqlFixture : OwnedNavigationsRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsPrimitiveCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsPrimitiveCollectionNpgsqlTest.cs deleted file mode 100644 index 725a747c00..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsPrimitiveCollectionNpgsqlTest.cs +++ /dev/null @@ -1,166 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsPrimitiveCollectionNpgsqlTest(OwnedNavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedNavigationsPrimitiveCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE cardinality(r0."Ints") = 3 -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE r0."Ints"[1] = 1 -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Contains() - { - await base.Contains(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE 3 = ANY (r0."Ints") -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Any_predicate() - { - await base.Any_predicate(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE 2 = ANY (r0."Ints") -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_Count() - { - await base.Nested_Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", r1."AssociateTypeRootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE cardinality(r1."Ints") = 3 -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Select_Sum() - { - await base.Select_Sum(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(i0.value), 0)::int - FROM unnest(r0."Ints") AS i0(value)) -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -WHERE ( - SELECT COALESCE(sum(i.value), 0)::int - FROM unnest(r0."Ints") AS i(value)) >= 6 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs deleted file mode 100644 index 24c01fda7d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionNpgsqlTest.cs +++ /dev/null @@ -1,447 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsProjectionNpgsqlTest(OwnedNavigationsNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedNavigationsProjectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #region Scalar properties - - public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r0."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -"""); - } - - public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_property_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT o."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -"""); - } - - public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_value_type_property_on_null_associate_throws(queryTrackingBehavior); - - AssertSql( - """ -SELECT o."Int" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -"""); - } - - public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_property_on_null_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT o."Int" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -"""); - } - - #endregion Scalar properties - - #region Structural properties - - public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."Id", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT o."RootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."Id", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1."AssociateTypeRootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -"""); - } - } - - public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1."AssociateTypeRootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -"""); - } - } - - public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT o0."AssociateTypeRootEntityId", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -"""); - } - } - - public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT o0."AssociateTypeRootEntityId", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -"""); - } - } - - public override async Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_associate_via_optional_navigation(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1."RootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r."Id", r0."Id", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r4."AssociateTypeRootEntityId", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String" -FROM "RootReferencingEntity" AS r -LEFT JOIN "RootEntity" AS r0 ON r."RootEntityId" = r0."Id" -LEFT JOIN "RequiredRelated" AS r1 ON r0."Id" = r1."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r1."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r3 ON r1."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r4 ON r1."RootEntityId" = r4."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."Id" NULLS FIRST, r1."RootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r4."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."Id", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT r0."Int" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -"""); - } - - #endregion Structural properties - - #region Collection - - public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate_collection(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r2."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r2."AssociateTypeId" AS "AssociateTypeId0", r3."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r3."AssociateTypeId" AS "AssociateTypeId1", r3."Id" AS "Id0", r3."Int" AS "Int0", r3."Ints" AS "Ints0", r3."Name" AS "Name0", r3."String" AS "String0", r1."Id" AS "Id1", r1."Int" AS "Int1", r1."Ints" AS "Ints1", r1."Name" AS "Name1", r1."String" AS "String1", r2."Id" AS "Id2", r2."Int" AS "Int2", r2."Ints" AS "Ints2", r2."Name" AS "Name2", r2."String" AS "String2" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" AND r0."Id" = r2."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" AND r0."Id" = r3."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST -"""); - } - } - - public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r0."RootEntityId", r1."AssociateTypeRootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", o."RootEntityId", o0."AssociateTypeRootEntityId", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_associate_collection(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."Id", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r2."AssociateTypeRootEntityId", r2."AssociateTypeId", r3."AssociateTypeRootEntityId", r3."AssociateTypeId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -INNER JOIN "RelatedCollection" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RelatedCollection_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -LEFT JOIN "RelatedCollection_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" AND r0."Id" = r2."AssociateTypeId" -LEFT JOIN "RelatedCollection_NestedCollection" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" AND r0."Id" = r3."AssociateTypeId" -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r0."Id" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeId" NULLS FIRST -"""); - } - } - - public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1."AssociateTypeRootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -INNER JOIN "RequiredRelated_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -"""); - } - } - - public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT o0."AssociateTypeRootEntityId", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -INNER JOIN "OptionalRelated_NestedCollection" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -"""); - } - } - - #endregion Collection - - #region Multiple - - public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_duplicated(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", s0."RootEntityId", s0."Id", s0."Int", s0."Ints", s0."Name", s0."String", s0."AssociateTypeRootEntityId", s0."AssociateTypeId", s0."AssociateTypeRootEntityId0", s0."AssociateTypeId0", s0."AssociateTypeRootEntityId1", s0."AssociateTypeId1", s0."Id0", s0."Int0", s0."Ints0", s0."Name0", s0."String0", s0."Id1", s0."Int1", s0."Ints1", s0."Name1", s0."String1", s0."Id2", s0."Int2", s0."Ints2", s0."Name2", s0."String2", o3."AssociateTypeRootEntityId", o3."Id", o3."Int", o3."Ints", o3."Name", o3."String", r12."AssociateTypeRootEntityId", r12."Id", r12."Int", r12."Ints", r12."Name", r12."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r8."RootEntityId", r8."Id", r8."Int", r8."Ints", r8."Name", r8."String", r9."AssociateTypeRootEntityId", r9."AssociateTypeId", r10."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r10."AssociateTypeId" AS "AssociateTypeId0", r11."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r11."AssociateTypeId" AS "AssociateTypeId1", r11."Id" AS "Id0", r11."Int" AS "Int0", r11."Ints" AS "Ints0", r11."Name" AS "Name0", r11."String" AS "String0", r9."Id" AS "Id1", r9."Int" AS "Int1", r9."Ints" AS "Ints1", r9."Name" AS "Name1", r9."String" AS "String1", r10."Id" AS "Id2", r10."Int" AS "Int2", r10."Ints" AS "Ints2", r10."Name" AS "Name2", r10."String" AS "String2" - FROM "RelatedCollection" AS r8 - LEFT JOIN "RelatedCollection_OptionalNested" AS r9 ON r8."RootEntityId" = r9."AssociateTypeRootEntityId" AND r8."Id" = r9."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r10 ON r8."RootEntityId" = r10."AssociateTypeRootEntityId" AND r8."Id" = r10."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r11 ON r8."RootEntityId" = r11."AssociateTypeRootEntityId" AND r8."Id" = r11."AssociateTypeId" -) AS s0 ON r."Id" = s0."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o3 ON o."RootEntityId" = o3."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r12 ON r0."RootEntityId" = r12."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST, r7."Id" NULLS FIRST, s0."RootEntityId" NULLS FIRST, s0."Id" NULLS FIRST, s0."AssociateTypeRootEntityId" NULLS FIRST, s0."AssociateTypeId" NULLS FIRST, s0."AssociateTypeRootEntityId0" NULLS FIRST, s0."AssociateTypeId0" NULLS FIRST, s0."AssociateTypeRootEntityId1" NULLS FIRST, s0."AssociateTypeId1" NULLS FIRST, s0."Id0" NULLS FIRST, o3."AssociateTypeRootEntityId" NULLS FIRST, o3."Id" NULLS FIRST, r12."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #endregion Multiple - - #region Subquery - - public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT s."AssociateTypeRootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" - FROM "RootEntity" AS r0 - LEFT JOIN "RequiredRelated" AS r1 ON r0."Id" = r1."RootEntityId" - LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r1."RootEntityId" = r2."AssociateTypeRootEntityId" - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS s ON TRUE -"""); - } - } - - public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT s."AssociateTypeRootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT o0."AssociateTypeRootEntityId", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String" - FROM "RootEntity" AS r0 - LEFT JOIN "OptionalRelated" AS o ON r0."Id" = o."RootEntityId" - LEFT JOIN "OptionalRelated_RequiredNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS s ON TRUE -"""); - } - } - - #endregion Subquery - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsSetOperationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsSetOperationsNpgsqlTest.cs deleted file mode 100644 index df2b2f459d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsSetOperationsNpgsqlTest.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsSetOperationsNpgsqlTest( - OwnedNavigationsNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedNavigationsSetOperationsRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Over_associate_collections() - { - await base.Over_associate_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r2."RootEntityId", r3."AssociateTypeRootEntityId", r4."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r9."AssociateTypeRootEntityId", r9."Id", r9."Int", r9."Ints", r9."Name", r9."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r2 ON r."Id" = r2."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r3 ON r2."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r4 ON r2."RootEntityId" = r4."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r5."RootEntityId", r5."Id", r5."Int", r5."Ints", r5."Name", r5."String", r6."AssociateTypeRootEntityId", r6."AssociateTypeId", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r7."AssociateTypeId" AS "AssociateTypeId0", r8."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r8."AssociateTypeId" AS "AssociateTypeId1", r8."Id" AS "Id0", r8."Int" AS "Int0", r8."Ints" AS "Ints0", r8."Name" AS "Name0", r8."String" AS "String0", r6."Id" AS "Id1", r6."Int" AS "Int1", r6."Ints" AS "Ints1", r6."Name" AS "Name1", r6."String" AS "String1", r7."Id" AS "Id2", r7."Int" AS "Int2", r7."Ints" AS "Ints2", r7."Name" AS "Name2", r7."String" AS "String2" - FROM "RelatedCollection" AS r5 - LEFT JOIN "RelatedCollection_OptionalNested" AS r6 ON r5."RootEntityId" = r6."AssociateTypeRootEntityId" AND r5."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r7 ON r5."RootEntityId" = r7."AssociateTypeRootEntityId" AND r5."Id" = r7."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r8 ON r5."RootEntityId" = r8."AssociateTypeRootEntityId" AND r5."Id" = r8."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r9 ON r2."RootEntityId" = r9."AssociateTypeRootEntityId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId" AND r0."Int" = 8 - UNION ALL - SELECT 1 - FROM "RelatedCollection" AS r1 - WHERE r."Id" = r1."RootEntityId" AND r1."String" = 'foo' - ) AS u) = 4 -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r2."RootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r4."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r9."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override Task Over_associate_collection_projected(QueryTrackingBehavior queryTrackingBehavior) - => Assert.ThrowsAnyAsync(() => base.Over_associate_collection_projected(queryTrackingBehavior)); - - public override async Task Over_assocate_collection_Select_nested_with_aggregates_projected(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Over_assocate_collection_Select_nested_with_aggregates_projected(queryTrackingBehavior); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(( - SELECT COALESCE(sum(r2."Int"), 0)::int - FROM "RelatedCollection_NestedCollection" AS r2 - WHERE u."RootEntityId" = r2."AssociateTypeRootEntityId" AND u."Id" = r2."AssociateTypeId")), 0)::int - FROM ( - SELECT r0."RootEntityId", r0."Id" - FROM "RelatedCollection" AS r0 - WHERE r."Id" = r0."RootEntityId" AND r0."Int" = 8 - UNION ALL - SELECT r1."RootEntityId", r1."Id" - FROM "RelatedCollection" AS r1 - WHERE r."Id" = r1."RootEntityId" AND r1."String" = 'foo' - ) AS u) -FROM "RootEntity" AS r -"""); - } - - public override async Task Over_nested_associate_collection() - { - await base.Over_nested_associate_collection(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r3."AssociateTypeRootEntityId", r4."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r9."AssociateTypeRootEntityId", r9."Id", r9."Int", r9."Ints", r9."Name", r9."String", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."Id", r4."Int", r4."Ints", r4."Name", r4."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r3 ON r0."RootEntityId" = r3."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r4 ON r0."RootEntityId" = r4."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r5."RootEntityId", r5."Id", r5."Int", r5."Ints", r5."Name", r5."String", r6."AssociateTypeRootEntityId", r6."AssociateTypeId", r7."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r7."AssociateTypeId" AS "AssociateTypeId0", r8."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r8."AssociateTypeId" AS "AssociateTypeId1", r8."Id" AS "Id0", r8."Int" AS "Int0", r8."Ints" AS "Ints0", r8."Name" AS "Name0", r8."String" AS "String0", r6."Id" AS "Id1", r6."Int" AS "Int1", r6."Ints" AS "Ints1", r6."Name" AS "Name1", r6."String" AS "String1", r7."Id" AS "Id2", r7."Int" AS "Int2", r7."Ints" AS "Ints2", r7."Name" AS "Name2", r7."String" AS "String2" - FROM "RelatedCollection" AS r5 - LEFT JOIN "RelatedCollection_OptionalNested" AS r6 ON r5."RootEntityId" = r6."AssociateTypeRootEntityId" AND r5."Id" = r6."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r7 ON r5."RootEntityId" = r7."AssociateTypeRootEntityId" AND r5."Id" = r7."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r8 ON r5."RootEntityId" = r8."AssociateTypeRootEntityId" AND r5."Id" = r8."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r9 ON r0."RootEntityId" = r9."AssociateTypeRootEntityId" -WHERE ( - SELECT count(*)::int - FROM ( - SELECT 1 - FROM "RequiredRelated_NestedCollection" AS r1 - WHERE r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r1."Int" = 8 - UNION ALL - SELECT 1 - FROM "RequiredRelated_NestedCollection" AS r2 - WHERE r0."RootEntityId" = r2."AssociateTypeRootEntityId" AND r2."String" = 'foo' - ) AS u) = 4 -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r3."AssociateTypeRootEntityId" NULLS FIRST, r4."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r9."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Over_different_collection_properties() - { - await base.Over_different_collection_properties(); - - AssertSql( -); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityNpgsqlTest.cs deleted file mode 100644 index 4df9d8bbc3..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityNpgsqlTest.cs +++ /dev/null @@ -1,272 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedNavigations; - -public class OwnedNavigationsStructuralEqualityNpgsqlTest( - OwnedNavigationsNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedNavigationsStructuralEqualityRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Two_associates() - { - await base.Two_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Two_nested_associates() - { - await base.Two_nested_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", r1."AssociateTypeRootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Not_equals() - { - await base.Not_equals(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE o."RootEntityId" IS NULL -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Associate_with_parameter_null() - { - await base.Associate_with_parameter_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE o."RootEntityId" IS NULL -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_inline_null() - { - await base.Nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", r0."RootEntityId", r1."AssociateTypeRootEntityId", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE r1."AssociateTypeRootEntityId" IS NULL -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_inline() - { - await base.Nested_associate_with_inline(); - - AssertSql( -); - } - - public override async Task Nested_associate_with_parameter() - { - await base.Nested_associate_with_parameter(); - - AssertSql( -); - } - - public override async Task Two_nested_collections() - { - await base.Two_nested_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", o."RootEntityId", o0."AssociateTypeRootEntityId", o1."AssociateTypeRootEntityId", r0."RootEntityId", r1."AssociateTypeRootEntityId", r2."AssociateTypeRootEntityId", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."AssociateTypeRootEntityId0", s."AssociateTypeId0", s."AssociateTypeRootEntityId1", s."AssociateTypeId1", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."Id1", s."Int1", s."Ints1", s."Name1", s."String1", s."Id2", s."Int2", s."Ints2", s."Name2", s."String2", o."Id", o."Int", o."Ints", o."Name", o."String", o2."AssociateTypeRootEntityId", o2."Id", o2."Int", o2."Ints", o2."Name", o2."String", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", o1."Id", o1."Int", o1."Ints", o1."Name", o1."String", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r7."AssociateTypeRootEntityId", r7."Id", r7."Int", r7."Ints", r7."Name", r7."String", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated" AS o ON r."Id" = o."RootEntityId" -LEFT JOIN "OptionalRelated_OptionalNested" AS o0 ON o."RootEntityId" = o0."AssociateTypeRootEntityId" -LEFT JOIN "OptionalRelated_RequiredNested" AS o1 ON o."RootEntityId" = o1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RequiredRelated_OptionalNested" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_RequiredNested" AS r2 ON r0."RootEntityId" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r5."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId0", r5."AssociateTypeId" AS "AssociateTypeId0", r6."AssociateTypeRootEntityId" AS "AssociateTypeRootEntityId1", r6."AssociateTypeId" AS "AssociateTypeId1", r6."Id" AS "Id0", r6."Int" AS "Int0", r6."Ints" AS "Ints0", r6."Name" AS "Name0", r6."String" AS "String0", r4."Id" AS "Id1", r4."Int" AS "Int1", r4."Ints" AS "Ints1", r4."Name" AS "Name1", r4."String" AS "String1", r5."Id" AS "Id2", r5."Int" AS "Int2", r5."Ints" AS "Ints2", r5."Name" AS "Name2", r5."String" AS "String2" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_OptionalNested" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" - LEFT JOIN "RelatedCollection_RequiredNested" AS r5 ON r3."RootEntityId" = r5."AssociateTypeRootEntityId" AND r3."Id" = r5."AssociateTypeId" - LEFT JOIN "RelatedCollection_NestedCollection" AS r6 ON r3."RootEntityId" = r6."AssociateTypeRootEntityId" AND r3."Id" = r6."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o2 ON o."RootEntityId" = o2."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r7 ON r0."RootEntityId" = r7."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, o."RootEntityId" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o1."AssociateTypeRootEntityId" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."AssociateTypeRootEntityId0" NULLS FIRST, s."AssociateTypeId0" NULLS FIRST, s."AssociateTypeRootEntityId1" NULLS FIRST, s."AssociateTypeId1" NULLS FIRST, s."Id0" NULLS FIRST, o2."AssociateTypeRootEntityId" NULLS FIRST, o2."Id" NULLS FIRST, r7."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_collection_with_inline() - { - await base.Nested_collection_with_inline(); - - AssertSql(); - } - - public override async Task Nested_collection_with_parameter() - { - await base.Nested_collection_with_parameter(); - - AssertSql(); - } - - #region Contains - - public override async Task Contains_with_inline() - { - await base.Contains_with_inline(); - - AssertSql(); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - AssertSql(); - } - - public override async Task Contains_with_operators_composed_on_the_collection() - { - await base.Contains_with_operators_composed_on_the_collection(); - - AssertSql(); - } - - public override async Task Contains_with_nested_and_composed_operators() - { - await base.Contains_with_nested_and_composed_operators(); - - AssertSql(); - } - - #endregion Contains - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingMiscellaneousNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingMiscellaneousNpgsqlTest.cs deleted file mode 100644 index 3efb878792..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingMiscellaneousNpgsqlTest.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedTableSplitting; - -public class OwnedTableSplittingMiscellaneousNpgsqlTest( - OwnedTableSplittingNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedTableSplittingMiscellaneousRelationalTestBase(fixture, testOutputHelper) -{ - #region Simple filters - - public override async Task Where_on_associate_scalar_property() - { - await base.Where_on_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE r."RequiredAssociate_Int" = 8 -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Where_on_optional_associate_scalar_property() - { - await base.Where_on_optional_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE r."OptionalAssociate_Int" = 8 -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Where_on_nested_associate_scalar_property() - { - await base.Where_on_nested_associate_scalar_property(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE r."RequiredAssociate_RequiredNestedAssociate_Int" = 8 -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #endregion Simple filters - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingNpgsqlFixture.cs deleted file mode 100644 index a71da6f46b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingNpgsqlFixture.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedTableSplitting; - -public class OwnedTableSplittingNpgsqlFixture : OwnedTableSplittingRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingPrimitiveCollectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingPrimitiveCollectionNpgsqlTest.cs deleted file mode 100644 index 690846515e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingPrimitiveCollectionNpgsqlTest.cs +++ /dev/null @@ -1,135 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedTableSplitting; - -public class OwnedTableSplittingPrimitiveCollectionNpgsqlTest(OwnedTableSplittingNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedTableSplittingPrimitiveCollectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Count() - { - await base.Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE cardinality(r."RequiredAssociate_Ints") = 3 -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE r."RequiredAssociate_Ints"[1] = 1 -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Contains() - { - await base.Contains(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE 3 = ANY (r."RequiredAssociate_Ints") -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Any_predicate() - { - await base.Any_predicate(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE 2 = ANY (r."RequiredAssociate_Ints") -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_Count() - { - await base.Nested_Count(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE cardinality(r."RequiredAssociate_RequiredNestedAssociate_Ints") = 3 -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Select_Sum() - { - await base.Select_Sum(); - - AssertSql( - """ -SELECT ( - SELECT COALESCE(sum(r1.value), 0)::int - FROM unnest(r."RequiredAssociate_Ints") AS r1(value)) -FROM "RootEntity" AS r -WHERE ( - SELECT COALESCE(sum(r0.value), 0)::int - FROM unnest(r."RequiredAssociate_Ints") AS r0(value)) >= 6 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs deleted file mode 100644 index 4000b9bb61..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingProjectionNpgsqlTest.cs +++ /dev/null @@ -1,404 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedTableSplitting; - -public class OwnedTableSplittingProjectionNpgsqlTest(OwnedTableSplittingNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : OwnedTableSplittingProjectionRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #region Simple properties - - public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_property_on_optional_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_String" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_value_type_property_on_null_associate_throws(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_Int" -FROM "RootEntity" AS r -"""); - } - - public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nullable_value_type_property_on_null_associate(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."OptionalAssociate_Int" -FROM "RootEntity" AS r -"""); - } - - #endregion Simple properties - - #region Non-collection - - public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String" -FROM "RootEntity" AS r -"""); - } - } - - public override async Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_required_associate_via_optional_navigation(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."Id", r0."RequiredAssociate_Id", r0."RequiredAssociate_Int", r0."RequiredAssociate_Ints", r0."RequiredAssociate_Name", r0."RequiredAssociate_String", r."Id", r1."AssociateTypeRootEntityId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r0."RequiredAssociate_OptionalNestedAssociate_Id", r0."RequiredAssociate_OptionalNestedAssociate_Int", r0."RequiredAssociate_OptionalNestedAssociate_Ints", r0."RequiredAssociate_OptionalNestedAssociate_Name", r0."RequiredAssociate_OptionalNestedAssociate_String", r0."RequiredAssociate_RequiredNestedAssociate_Id", r0."RequiredAssociate_RequiredNestedAssociate_Int", r0."RequiredAssociate_RequiredNestedAssociate_Ints", r0."RequiredAssociate_RequiredNestedAssociate_Name", r0."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootReferencingEntity" AS r -LEFT JOIN "RootEntity" AS r0 ON r."RootEntityId" = r0."Id" -LEFT JOIN "RequiredRelated_NestedCollection" AS r1 ON r0."Id" = r1."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."Id" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."RequiredAssociate_Int" -FROM "RootEntity" AS r -"""); - } - - #endregion Non-collection - - #region Collection - - public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_associate_collection(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST -"""); - } - } - - public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String" -FROM "RootEntity" AS r -LEFT JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, r0."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r."Id", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String" -FROM "RootEntity" AS r -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - } - - public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_associate_collection(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r."Id", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id", r1."Int", r1."Ints", r1."Name", r1."String", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" -FROM "RootEntity" AS r -INNER JOIN "RelatedCollection" AS r0 ON r."Id" = r0."RootEntityId" -LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -ORDER BY r."Id" NULLS FIRST, r0."RootEntityId" NULLS FIRST, r0."Id" NULLS FIRST, r1."AssociateTypeRootEntityId" NULLS FIRST, r1."AssociateTypeId" NULLS FIRST -"""); - } - } - - public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r0."AssociateTypeRootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String" -FROM "RootEntity" AS r -INNER JOIN "RequiredRelated_NestedCollection" AS r0 ON r."Id" = r0."AssociateTypeRootEntityId" -"""); - } - } - - public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) - { - await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String" -FROM "RootEntity" AS r -INNER JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -"""); - } - } - - #endregion Collection - - #region Multiple - - public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_root_duplicated(queryTrackingBehavior); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String", s0."RootEntityId", s0."Id", s0."Int", s0."Ints", s0."Name", s0."String", s0."AssociateTypeRootEntityId", s0."AssociateTypeId", s0."Id0", s0."Int0", s0."Ints0", s0."Name0", s0."String0", s0."OptionalNestedAssociate_Id", s0."OptionalNestedAssociate_Int", s0."OptionalNestedAssociate_Ints", s0."OptionalNestedAssociate_Name", s0."OptionalNestedAssociate_String", s0."RequiredNestedAssociate_Id", s0."RequiredNestedAssociate_Int", s0."RequiredNestedAssociate_Ints", s0."RequiredNestedAssociate_Name", s0."RequiredNestedAssociate_String", o0."AssociateTypeRootEntityId", o0."Id", o0."Int", o0."Ints", o0."Name", o0."String", r5."AssociateTypeRootEntityId", r5."Id", r5."Int", r5."Ints", r5."Name", r5."String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -LEFT JOIN ( - SELECT r3."RootEntityId", r3."Id", r3."Int", r3."Ints", r3."Name", r3."String", r4."AssociateTypeRootEntityId", r4."AssociateTypeId", r4."Id" AS "Id0", r4."Int" AS "Int0", r4."Ints" AS "Ints0", r4."Name" AS "Name0", r4."String" AS "String0", r3."OptionalNestedAssociate_Id", r3."OptionalNestedAssociate_Int", r3."OptionalNestedAssociate_Ints", r3."OptionalNestedAssociate_Name", r3."OptionalNestedAssociate_String", r3."RequiredNestedAssociate_Id", r3."RequiredNestedAssociate_Int", r3."RequiredNestedAssociate_Ints", r3."RequiredNestedAssociate_Name", r3."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r3 - LEFT JOIN "RelatedCollection_NestedCollection" AS r4 ON r3."RootEntityId" = r4."AssociateTypeRootEntityId" AND r3."Id" = r4."AssociateTypeId" -) AS s0 ON r."Id" = s0."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o0 ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o0."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r5 ON r."Id" = r5."AssociateTypeRootEntityId" -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST, r2."Id" NULLS FIRST, s0."RootEntityId" NULLS FIRST, s0."Id" NULLS FIRST, s0."AssociateTypeRootEntityId" NULLS FIRST, s0."AssociateTypeId" NULLS FIRST, s0."Id0" NULLS FIRST, o0."AssociateTypeRootEntityId" NULLS FIRST, o0."Id" NULLS FIRST, r5."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - #endregion Multiple - - #region Subquery - - public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1."Id", r1."RequiredAssociate_RequiredNestedAssociate_Id", r1."RequiredAssociate_RequiredNestedAssociate_Int", r1."RequiredAssociate_RequiredNestedAssociate_Ints", r1."RequiredAssociate_RequiredNestedAssociate_Name", r1."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."Id", r0."RequiredAssociate_RequiredNestedAssociate_Id", r0."RequiredAssociate_RequiredNestedAssociate_Int", r0."RequiredAssociate_RequiredNestedAssociate_Ints", r0."RequiredAssociate_RequiredNestedAssociate_Name", r0."RequiredAssociate_RequiredNestedAssociate_String" - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - } - - public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) - { - await base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior); - - if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) - { - AssertSql( - """ -SELECT r1."Id", r1."OptionalAssociate_RequiredNestedAssociate_Id", r1."OptionalAssociate_RequiredNestedAssociate_Int", r1."OptionalAssociate_RequiredNestedAssociate_Ints", r1."OptionalAssociate_RequiredNestedAssociate_Name", r1."OptionalAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN LATERAL ( - SELECT r0."Id", r0."OptionalAssociate_RequiredNestedAssociate_Id", r0."OptionalAssociate_RequiredNestedAssociate_Int", r0."OptionalAssociate_RequiredNestedAssociate_Ints", r0."OptionalAssociate_RequiredNestedAssociate_Name", r0."OptionalAssociate_RequiredNestedAssociate_String" - FROM "RootEntity" AS r0 - ORDER BY r0."Id" NULLS FIRST - LIMIT 1 -) AS r1 ON TRUE -"""); - } - } - - #endregion Subquery - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualityNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualityNpgsqlTest.cs deleted file mode 100644 index 772119fcd5..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Associations/OwnedTableSplitting/OwnedTableSplittingStructuralEqualityNpgsqlTest.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedTableSplitting; - -public class OwnedTableSplittingStructuralEqualityNpgsqlTest( - OwnedTableSplittingNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : OwnedTableSplittingStructuralEqualityRelationalTestBase(fixture, testOutputHelper) -{ - public override async Task Two_associates() - { - await base.Two_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Two_nested_associates() - { - await base.Two_nested_associates(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Not_equals() - { - await base.Not_equals(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Associate_with_inline_null() - { - await base.Associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE r."OptionalAssociate_Id" IS NULL OR r."OptionalAssociate_Int" IS NULL OR r."OptionalAssociate_Ints" IS NULL OR r."OptionalAssociate_Name" IS NULL OR r."OptionalAssociate_String" IS NULL -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Associate_with_parameter_null() - { - await base.Associate_with_parameter_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END IS NULL -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_inline_null() - { - await base.Nested_associate_with_inline_null(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE r."RequiredAssociate_OptionalNestedAssociate_Id" IS NULL OR r."RequiredAssociate_OptionalNestedAssociate_Int" IS NULL OR r."RequiredAssociate_OptionalNestedAssociate_Ints" IS NULL OR r."RequiredAssociate_OptionalNestedAssociate_Name" IS NULL OR r."RequiredAssociate_OptionalNestedAssociate_String" IS NULL -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_associate_with_inline() - { - await base.Nested_associate_with_inline(); - - AssertSql( -); - } - - public override async Task Nested_associate_with_parameter() - { - await base.Nested_associate_with_parameter(); - - AssertSql( -); - } - - public override async Task Two_nested_collections() - { - await base.Two_nested_collections(); - - AssertSql( - """ -SELECT r."Id", r."Name", s."RootEntityId", s."Id", s."Int", s."Ints", s."Name", s."String", s."AssociateTypeRootEntityId", s."AssociateTypeId", s."Id0", s."Int0", s."Ints0", s."Name0", s."String0", s."OptionalNestedAssociate_Id", s."OptionalNestedAssociate_Int", s."OptionalNestedAssociate_Ints", s."OptionalNestedAssociate_Name", s."OptionalNestedAssociate_String", s."RequiredNestedAssociate_Id", s."RequiredNestedAssociate_Int", s."RequiredNestedAssociate_Ints", s."RequiredNestedAssociate_Name", s."RequiredNestedAssociate_String", r."OptionalAssociate_Id", r."OptionalAssociate_Int", r."OptionalAssociate_Ints", r."OptionalAssociate_Name", r."OptionalAssociate_String", o."AssociateTypeRootEntityId", o."Id", o."Int", o."Ints", o."Name", o."String", r."OptionalAssociate_OptionalNestedAssociate_Id", r."OptionalAssociate_OptionalNestedAssociate_Int", r."OptionalAssociate_OptionalNestedAssociate_Ints", r."OptionalAssociate_OptionalNestedAssociate_Name", r."OptionalAssociate_OptionalNestedAssociate_String", r."OptionalAssociate_RequiredNestedAssociate_Id", r."OptionalAssociate_RequiredNestedAssociate_Int", r."OptionalAssociate_RequiredNestedAssociate_Ints", r."OptionalAssociate_RequiredNestedAssociate_Name", r."OptionalAssociate_RequiredNestedAssociate_String", r."RequiredAssociate_Id", r."RequiredAssociate_Int", r."RequiredAssociate_Ints", r."RequiredAssociate_Name", r."RequiredAssociate_String", r2."AssociateTypeRootEntityId", r2."Id", r2."Int", r2."Ints", r2."Name", r2."String", r."RequiredAssociate_OptionalNestedAssociate_Id", r."RequiredAssociate_OptionalNestedAssociate_Int", r."RequiredAssociate_OptionalNestedAssociate_Ints", r."RequiredAssociate_OptionalNestedAssociate_Name", r."RequiredAssociate_OptionalNestedAssociate_String", r."RequiredAssociate_RequiredNestedAssociate_Id", r."RequiredAssociate_RequiredNestedAssociate_Int", r."RequiredAssociate_RequiredNestedAssociate_Ints", r."RequiredAssociate_RequiredNestedAssociate_Name", r."RequiredAssociate_RequiredNestedAssociate_String" -FROM "RootEntity" AS r -LEFT JOIN ( - SELECT r0."RootEntityId", r0."Id", r0."Int", r0."Ints", r0."Name", r0."String", r1."AssociateTypeRootEntityId", r1."AssociateTypeId", r1."Id" AS "Id0", r1."Int" AS "Int0", r1."Ints" AS "Ints0", r1."Name" AS "Name0", r1."String" AS "String0", r0."OptionalNestedAssociate_Id", r0."OptionalNestedAssociate_Int", r0."OptionalNestedAssociate_Ints", r0."OptionalNestedAssociate_Name", r0."OptionalNestedAssociate_String", r0."RequiredNestedAssociate_Id", r0."RequiredNestedAssociate_Int", r0."RequiredNestedAssociate_Ints", r0."RequiredNestedAssociate_Name", r0."RequiredNestedAssociate_String" - FROM "RelatedCollection" AS r0 - LEFT JOIN "RelatedCollection_NestedCollection" AS r1 ON r0."RootEntityId" = r1."AssociateTypeRootEntityId" AND r0."Id" = r1."AssociateTypeId" -) AS s ON r."Id" = s."RootEntityId" -LEFT JOIN "OptionalRelated_NestedCollection" AS o ON CASE - WHEN r."OptionalAssociate_Id" IS NOT NULL AND r."OptionalAssociate_Int" IS NOT NULL AND r."OptionalAssociate_Ints" IS NOT NULL AND r."OptionalAssociate_Name" IS NOT NULL AND r."OptionalAssociate_String" IS NOT NULL THEN r."Id" -END = o."AssociateTypeRootEntityId" -LEFT JOIN "RequiredRelated_NestedCollection" AS r2 ON r."Id" = r2."AssociateTypeRootEntityId" -WHERE FALSE -ORDER BY r."Id" NULLS FIRST, s."RootEntityId" NULLS FIRST, s."Id" NULLS FIRST, s."AssociateTypeRootEntityId" NULLS FIRST, s."AssociateTypeId" NULLS FIRST, s."Id0" NULLS FIRST, o."AssociateTypeRootEntityId" NULLS FIRST, o."Id" NULLS FIRST, r2."AssociateTypeRootEntityId" NULLS FIRST -"""); - } - - public override async Task Nested_collection_with_inline() - { - await base.Nested_collection_with_inline(); - - AssertSql(); - } - - public override async Task Nested_collection_with_parameter() - { - await base.Nested_collection_with_parameter(); - - AssertSql(); - } - - #region Contains - - public override async Task Contains_with_inline() - { - await base.Contains_with_inline(); - - AssertSql(); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - AssertSql(); - } - - public override async Task Contains_with_operators_composed_on_the_collection() - { - await base.Contains_with_operators_composed_on_the_collection(); - - AssertSql(); - } - - public override async Task Contains_with_nested_and_composed_operators() - { - await base.Contains_with_nested_and_composed_operators(); - - AssertSql(); - } - - #endregion Contains - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs deleted file mode 100644 index f23c0e9d25..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs +++ /dev/null @@ -1,225 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class CharacterQueryNpgsqlTest : IClassFixture -{ - private CharacterQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public CharacterQueryNpgsqlTest(CharacterQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [Fact] - public void Find_in_database() - { - Fixture.ClearEntities(); - - // important: add here so they aren't locally available below. - using (var ctx = CreateContext()) - { - ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "12345678" }); - ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "123456 " }); - ctx.SaveChanges(); - } - - using (var ctx = CreateContext()) - { - const string update = "update"; - - var m1 = ctx.CharacterTestEntities.Find("12345678"); - Assert.NotNull(m1); - m1.Character6 = update; - ctx.SaveChanges(); - - var m2 = ctx.CharacterTestEntities.Find("123456 "); - Assert.NotNull(m2); - m2.Character6 = update; - ctx.SaveChanges(); - - var item0 = ctx.CharacterTestEntities.Find("12345678")!.Character6; - Assert.Equal(update, item0); - - var item1 = ctx.CharacterTestEntities.Find("123456 ")!.Character6; - Assert.Equal(update, item1); - } - } - - [Fact] - public void Find_locally_available() - { - Fixture.ClearEntities(); - - // important: add here so they are locally available below. - using var ctx = CreateContext(); - ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "12345678" }); - ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "123456 " }); - ctx.SaveChanges(); - - const string update = "update"; - - var m1 = ctx.CharacterTestEntities.Find("12345678")!; - m1.Character6 = update; - ctx.SaveChanges(); - - var m2 = ctx.CharacterTestEntities.Find("123456 ")!; - m2.Character6 = update; - ctx.SaveChanges(); - - var item0 = ctx.CharacterTestEntities.Find("12345678")!.Character6; - Assert.Equal(update, item0); - - var item1 = ctx.CharacterTestEntities.Find("123456 ")!.Character6; - Assert.Equal(update, item1); - } - - /// - /// Test something like: select '123456 '::char(8) = '123456'::char(8); - /// - [Fact] - public void Test_change_tracking() - { - Fixture.ClearEntities(); - - using var ctx = CreateContext(); - const string update = "update"; - - ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "12345678" }); - ctx.CharacterTestEntities.Add(new CharacterTestEntity { Character8 = "123456 " }); - ctx.SaveChanges(); - - var m1 = ctx.CharacterTestEntities.Find("12345678")!; - m1.Character6 = update; - ctx.SaveChanges(); - - var m2 = ctx.CharacterTestEntities.Find("123456 ")!; - m2.Character6 = update; - ctx.SaveChanges(); - } - - /// - /// Test that comparisons are treated correctly. - /// - [Fact] - public void Test_change_tracking_key_sizes() - { - Fixture.ClearEntities(); - - using (var ctx = CreateContext()) - { - var entity = new CharacterTestEntity { Character8 = "123456 ", Character6 = "12345 " }; - ctx.CharacterTestEntities.Add(entity); - ctx.SaveChanges(); - - // In memory, the properties are unchanged. - Assert.Equal("12345 ", entity.Character6); - - // Trailing whitespace is ignored when querying. - var fromLocal = ctx.CharacterTestEntities.Single(x => x.Character6 == "12345"); - - // And since we queried the same context, we received the same object. - Assert.Equal(entity, fromLocal); - - // Which means that the property actually still has trailing whitespace... - Assert.Equal("12345 ", fromLocal.Character6); - - // No changes are detected/saved when trailing whitespace is added. - entity.Character6 += " "; - Assert.Equal(0, ctx.SaveChanges()); - } - - using (var ctx = CreateContext()) - { - // The query still ignores the trailing whitespace, - // but the materialized object won't have any trailing whitespace. - var fromDb = ctx.CharacterTestEntities.Single(x => x.Character6 == "12345 "); - - // BUG: Why isn't the local cache clean? This shouldn't have the trailing whitespace. - Assert.Equal("12345 ", fromDb.Character6); - } - } - - #region Fixture - - // ReSharper disable once ClassNeverInstantiated.Global - /// - /// Represents a fixture suitable for testing character data. - /// - public class CharacterQueryNpgsqlFixture : SharedStoreFixtureBase - { - protected override string StoreName - => "CharacterQueryNpgsqlTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - /// - /// Clears the entities in the context. - /// - public void ClearEntities() - { - using var ctx = CreateContext(); - var entities = ctx.CharacterTestEntities.ToArray(); - foreach (var e in entities) - { - ctx.CharacterTestEntities.Remove(e); - } - - ctx.SaveChanges(); - } - } - - public class CharacterTestEntity - { - public string? Character8 { get; set; } - public string? Character6 { get; set; } - } - - public class CharacterContext : PoolableDbContext - { - public DbSet CharacterTestEntities { get; set; } - - /// - /// Initializes a . - /// - /// - /// The options to be used for configuration. - /// - public CharacterContext(DbContextOptions options) - : base(options) - { - } - - /// - protected override void OnModelCreating(ModelBuilder builder) - => builder.Entity( - entity => - { - entity.HasKey(e => e.Character8); - entity.Property(e => e.Character8).HasColumnType("character(8)"); - entity.Property(e => e.Character6).HasColumnType("character(6)"); - }); - } - - #endregion - - #region Helpers - - protected CharacterContext CreateContext() - => Fixture.CreateContext(); - - // ReSharper disable once UnusedMember.Global - /// - /// 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, Fixture.TestSqlLoggerFactory.Sql); - - #endregion -} diff --git a/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs deleted file mode 100644 index dbe9435730..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/CompatibilityQueryNpgsqlTest.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class CompatibilityQueryNpgsqlTest : IClassFixture -{ - private CompatibilityQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public CompatibilityQueryNpgsqlTest(CompatibilityQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalFact] - public async Task Array_contains_is_not_parameterized_with_array_on_redshift() - { - var ctx = CreateRedshiftContext(); - - var numbers = new[] { 8, 9 }; - var result = await ctx.TestEntities.Where(e => numbers.Contains(e.SomeInt)).SingleAsync(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@numbers1='?' (DbType = Int32) -@numbers2='?' (DbType = Int32) - -SELECT t."Id", t."SomeInt" -FROM "TestEntities" AS t -WHERE t."SomeInt" IN (@numbers1, @numbers2) -LIMIT 2 -"""); - } - - #region Support - - private CompatibilityContext CreateContext(Version? postgresVersion = null) - => Fixture.CreateContext(postgresVersion); - - private CompatibilityContext CreateRedshiftContext() - => Fixture.CreateRedshiftContext(); - - public class CompatibilityQueryNpgsqlFixture : FixtureBase, IDisposable, IAsyncLifetime - { - private TestStore _testStore = null!; - - private const string StoreName = "CompatibilityTest"; - private readonly ListLoggerFactory _listLoggerFactory = NpgsqlTestStoreFactory.Instance.CreateListLoggerFactory(_ => false); - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)_listLoggerFactory; - - public virtual CompatibilityContext CreateContext() - => CreateContext(null); - - public virtual CompatibilityContext CreateContext(Version? postgresVersion) - { - var builder = new DbContextOptionsBuilder(); - _testStore.AddProviderOptions(builder); - builder - .UseNpgsql(o => o.SetPostgresVersion(postgresVersion)) - .UseLoggerFactory(_listLoggerFactory); - return new CompatibilityContext(builder.Options); - } - - public virtual CompatibilityContext CreateRedshiftContext() - { - var builder = new DbContextOptionsBuilder(); - _testStore.AddProviderOptions(builder); - builder - .UseNpgsql(o => o.UseRedshift()) - .UseLoggerFactory(_listLoggerFactory); - return new CompatibilityContext(builder.Options); - } - - public virtual async Task InitializeAsync() - { - _testStore = NpgsqlTestStoreFactory.Instance.GetOrCreate(StoreName); - await _testStore.InitializeAsync(null, CreateContext, c => CompatibilityContext.SeedAsync((CompatibilityContext)c)); - } - - // Called after DisposeAsync - public virtual void Dispose() - { - } - - public virtual async Task DisposeAsync() - => await _testStore.DisposeAsync(); - } - - public class CompatibilityTestEntity - { - public int Id { get; set; } - public int SomeInt { get; set; } - } - - public class CompatibilityContext(DbContextOptions options) : DbContext(options) - { - public DbSet TestEntities { get; set; } - - public static async Task SeedAsync(CompatibilityContext context) - { - context.TestEntities.AddRange( - new CompatibilityTestEntity { Id = 1, SomeInt = 8 }, - new CompatibilityTestEntity { Id = 2, SomeInt = 10 }); - await context.SaveChangesAsync(); - } - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - #endregion Support -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsQueryNpgsqlTest.cs deleted file mode 100644 index 34181c0da8..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsQueryNpgsqlTest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsCollectionsQueryNpgsqlTest : ComplexNavigationsCollectionsQueryRelationalTestBase< - ComplexNavigationsQueryNpgsqlFixture> -{ - public ComplexNavigationsCollectionsQueryNpgsqlTest( - ComplexNavigationsQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQueryNpgsqlTest.cs deleted file mode 100644 index f9fc7a7b13..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQueryNpgsqlTest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsCollectionsSharedTypeQueryNpgsqlTest : ComplexNavigationsCollectionsSharedTypeQueryRelationalTestBase< - ComplexNavigationsSharedTypeQueryNpgsqlFixture> -{ - public ComplexNavigationsCollectionsSharedTypeQueryNpgsqlTest( - ComplexNavigationsSharedTypeQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQueryNpgsqlTest.cs deleted file mode 100644 index 915f1132b8..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQueryNpgsqlTest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsCollectionsSplitQueryNpgsqlTest : ComplexNavigationsCollectionsSplitQueryRelationalTestBase< - ComplexNavigationsQueryNpgsqlFixture> -{ - public ComplexNavigationsCollectionsSplitQueryNpgsqlTest( - ComplexNavigationsQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryNpgsqlTest.cs deleted file mode 100644 index 4fe0eb4eb9..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsCollectionsSplitSharedTypeQueryNpgsqlTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsCollectionsSplitSharedTypeQueryNpgsqlTest : - ComplexNavigationsCollectionsSplitSharedTypeQueryRelationalTestBase< - ComplexNavigationsSharedTypeQueryNpgsqlFixture> -{ - public ComplexNavigationsCollectionsSplitSharedTypeQueryNpgsqlTest( - ComplexNavigationsSharedTypeQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlFixture.cs deleted file mode 100644 index 59cb452338..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlFixture.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsQueryNpgsqlFixture : ComplexNavigationsQueryRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(l => l.Date).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(l => l.Date).HasColumnType("timestamp without time zone"); - } - // public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - // { - // var optionsBuilder = base.AddOptions(builder); - // new NpgsqlDbContextOptionsBuilder(optionsBuilder).ReverseNullOrdering(); - // return optionsBuilder; - // } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs deleted file mode 100644 index 6a20b5a05e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsQueryNpgsqlTest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsQueryNpgsqlTest : ComplexNavigationsQueryRelationalTestBase -{ - public ComplexNavigationsQueryNpgsqlTest(ComplexNavigationsQueryNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - 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)); - - public override Task GroupJoin_client_method_in_OrderBy(bool async) - => AssertTranslationFailedWithDetails( - () => base.GroupJoin_client_method_in_OrderBy(async), - CoreStrings.QueryUnableToTranslateMethod( - "Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase", - "ClientMethodNullableInt")); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlFixture.cs deleted file mode 100644 index ad713cbf99..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlFixture.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsSharedTypeQueryNpgsqlFixture : ComplexNavigationsSharedTypeQueryRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(l => l.Date).HasColumnType("timestamp without time zone"); - modelBuilder.Entity("Level1.OneToOne_Required_PK1#Level2").Property("Date").HasColumnType("timestamp without time zone"); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs deleted file mode 100644 index fc8c7346a2..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryNpgsqlTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexNavigationsSharedTypeQueryNpgsqlTest - : ComplexNavigationsSharedTypeQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public ComplexNavigationsSharedTypeQueryNpgsqlTest( - ComplexNavigationsSharedTypeQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [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)); - - public override Task GroupJoin_client_method_in_OrderBy(bool async) - => AssertTranslationFailedWithDetails( - () => base.GroupJoin_client_method_in_OrderBy(async), - CoreStrings.QueryUnableToTranslateMethod( - "Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase", - "ClientMethodNullableInt")); - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/26104")] - public override Task GroupBy_aggregate_where_required_relationship(bool async) - => base.GroupBy_aggregate_where_required_relationship(async); - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/issues/26104")] - public override Task GroupBy_aggregate_where_required_relationship_2(bool async) - => base.GroupBy_aggregate_where_required_relationship_2(async); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs deleted file mode 100644 index 73559df0f1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ComplexTypeQueryNpgsqlTest.cs +++ /dev/null @@ -1,1153 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class ComplexTypeQueryNpgsqlTest : ComplexTypeQueryRelationalTestBase< - ComplexTypeQueryNpgsqlTest.ComplexTypeQueryNpgsqlFixture> -{ - public ComplexTypeQueryNpgsqlTest(ComplexTypeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Filter_on_property_inside_complex_type_after_subquery(bool async) - { - await base.Filter_on_property_inside_complex_type_after_subquery(async); - - AssertSql( - """ -@p='1' - -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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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 -) AS c0 -WHERE c0."ShippingAddress_ZipCode" = 7728 -"""); - } - - public override async Task Filter_on_property_inside_nested_complex_type_after_subquery(bool async) - { - await base.Filter_on_property_inside_nested_complex_type_after_subquery(async); - - AssertSql( - """ -@p='1' - -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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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 -) AS c0 -WHERE c0."ShippingAddress_Country_Code" = 'DE' -"""); - } - - public override async Task Filter_on_required_property_inside_required_complex_type_on_optional_navigation(bool async) - { - await base.Filter_on_required_property_inside_required_complex_type_on_optional_navigation(async); - - AssertSql( - """ -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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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."OptionalAddress_AddressLine1", c1."OptionalAddress_AddressLine2", c1."OptionalAddress_Tags", c1."OptionalAddress_ZipCode", c1."OptionalAddress_Country_Code", c1."OptionalAddress_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" -WHERE c0."ShippingAddress_ZipCode" <> 7728 OR c0."ShippingAddress_ZipCode" IS NULL -"""); - } - - public override async Task Filter_on_required_property_inside_required_complex_type_on_required_navigation(bool async) - { - await base.Filter_on_required_property_inside_required_complex_type_on_required_navigation(async); - - AssertSql( - """ -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."OptionalAddress_AddressLine1", c1."OptionalAddress_AddressLine2", c1."OptionalAddress_Tags", c1."OptionalAddress_ZipCode", c1."OptionalAddress_Country_Code", c1."OptionalAddress_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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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" -WHERE c0."ShippingAddress_ZipCode" <> 7728 -"""); - } - - public override async Task Project_complex_type_via_optional_navigation(bool async) - { - await base.Project_complex_type_via_optional_navigation(async); - - AssertSql( - """ -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" -"""); - } - - public override async Task Project_complex_type_via_required_navigation(bool async) - { - await base.Project_complex_type_via_required_navigation(async); - - AssertSql( - """ -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" -"""); - } - - public override async Task Load_complex_type_after_subquery_on_entity_type(bool async) - { - await base.Load_complex_type_after_subquery_on_entity_type(async); - - AssertSql( - """ -@p='1' - -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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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 -) AS c0 -"""); - } - - public override async Task Select_complex_type(bool async) - { - await base.Select_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" -FROM "Customer" AS c -"""); - } - - public override async Task Select_nested_complex_type(bool async) - { - await base.Select_nested_complex_type(async); - - AssertSql( - """ -SELECT c."ShippingAddress_Country_Code", c."ShippingAddress_Country_FullName" -FROM "Customer" AS c -"""); - } - - public override async Task Select_single_property_on_nested_complex_type(bool async) - { - await base.Select_single_property_on_nested_complex_type(async); - - AssertSql( - """ -SELECT c."ShippingAddress_Country_FullName" -FROM "Customer" AS c -"""); - } - - public override async Task Select_complex_type_Where(bool async) - { - await base.Select_complex_type_Where(async); - - AssertSql( - """ -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 -"""); - } - - public override async Task Select_complex_type_Distinct(bool async) - { - await base.Select_complex_type_Distinct(async); - - AssertSql( - """ -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 -"""); - } - - public override async Task Complex_type_equals_complex_type(bool async) - { - await base.Complex_type_equals_complex_type(async); - - AssertSql( - """ -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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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_Tags" = c."BillingAddress_Tags" AND c."ShippingAddress_ZipCode" = c."BillingAddress_ZipCode" -"""); - } - - public override async Task Complex_type_equals_constant(bool async) - { - await base.Complex_type_equals_constant(async); - - AssertSql( - """ -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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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_Tags" = ARRAY['foo','bar']::text[] AND c."ShippingAddress_ZipCode" = 38654 AND c."ShippingAddress_Country_Code" = 'US' AND c."ShippingAddress_Country_FullName" = 'United States' -"""); - } - - public override async Task Complex_type_equals_parameter(bool async) - { - await base.Complex_type_equals_parameter(async); - - AssertSql( - """ -@entity_equality_address_AddressLine1='804 S. Lakeshore Road' -@entity_equality_address_Tags={ 'foo' -'bar' } (DbType = Object) -@entity_equality_address_ZipCode='38654' (Nullable = true) -@entity_equality_address_Country_Code='US' -@entity_equality_address_Country_FullName='United States' - -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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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_AddressLine1 AND c."ShippingAddress_AddressLine2" IS NULL AND c."ShippingAddress_Tags" = @entity_equality_address_Tags AND c."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND c."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND c."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName -"""); - } - - public override async Task Subquery_over_complex_type(bool async) - { - await base.Subquery_over_complex_type(async); - - AssertSql(); - } - - public override async Task Contains_over_complex_type(bool async) - { - await base.Contains_over_complex_type(async); - - AssertSql( - """ -@entity_equality_address_AddressLine1='804 S. Lakeshore Road' -@entity_equality_address_Tags={ 'foo' -'bar' } (DbType = Object) -@entity_equality_address_ZipCode='38654' (Nullable = true) -@entity_equality_address_Country_Code='US' -@entity_equality_address_Country_FullName='United States' - -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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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_AddressLine1 AND c0."ShippingAddress_AddressLine2" IS NULL AND c0."ShippingAddress_Tags" = @entity_equality_address_Tags AND c0."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND c0."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND c0."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName) -"""); - } - - public override async Task Concat_complex_type(bool async) - { - await base.Concat_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" -FROM "Customer" AS c -WHERE c."Id" = 1 -UNION ALL -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 -"""); - } - - public override async Task Concat_entity_type_containing_complex_property(bool async) - { - await base.Concat_entity_type_containing_complex_property(async); - - AssertSql( - """ -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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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 -"""); - } - - public override async Task Union_entity_type_containing_complex_property(bool async) - { - await base.Union_entity_type_containing_complex_property(async); - - AssertSql( - """ -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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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_Tags", c0."BillingAddress_ZipCode", c0."BillingAddress_Country_Code", c0."BillingAddress_Country_FullName", c0."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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 -"""); - } - - public override async Task Union_complex_type(bool async) - { - await base.Union_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" -FROM "Customer" AS c -WHERE c."Id" = 1 -UNION -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 -"""); - } - - public override async Task Concat_property_in_complex_type(bool async) - { - await base.Concat_property_in_complex_type(async); - - AssertSql( - """ -SELECT c."ShippingAddress_AddressLine1" -FROM "Customer" AS c -UNION ALL -SELECT c0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" -FROM "Customer" AS c0 -"""); - } - - public override async Task Union_property_in_complex_type(bool async) - { - await base.Union_property_in_complex_type(async); - - AssertSql( - """ -SELECT c."ShippingAddress_AddressLine1" -FROM "Customer" AS c -UNION -SELECT c0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" -FROM "Customer" AS c0 -"""); - } - - public override async Task Concat_two_different_complex_type(bool async) - { - await base.Concat_two_different_complex_type(async); - - AssertSql(); - } - - public override async Task Union_two_different_complex_type(bool async) - { - await base.Union_two_different_complex_type(async); - - AssertSql(); - } - - public override async Task Filter_on_property_inside_struct_complex_type(bool async) - { - await base.Filter_on_property_inside_struct_complex_type(async); - - AssertSql( - """ -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_ZipCode" = 7728 -"""); - } - - public override async Task Filter_on_property_inside_nested_struct_complex_type(bool async) - { - await base.Filter_on_property_inside_nested_struct_complex_type(async); - - AssertSql( - """ -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_Country_Code" = 'DE' -"""); - } - - public override async Task Filter_on_property_inside_struct_complex_type_after_subquery(bool async) - { - await base.Filter_on_property_inside_struct_complex_type_after_subquery(async); - - AssertSql( - """ -@p='1' - -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 -) AS v0 -WHERE v0."ShippingAddress_ZipCode" = 7728 -"""); - } - - public override async Task Filter_on_property_inside_nested_struct_complex_type_after_subquery(bool async) - { - await base.Filter_on_property_inside_nested_struct_complex_type_after_subquery(async); - - AssertSql( - """ -@p='1' - -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 -) AS v0 -WHERE v0."ShippingAddress_Country_Code" = 'DE' -"""); - } - - public override async Task Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(bool async) - { - await base.Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(async); - - AssertSql( - """ -SELECT v."Id", v."OptionalCustomerId", v."RequiredCustomerId", 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", v1."Id", v1."Name", v1."BillingAddress_AddressLine1", v1."BillingAddress_AddressLine2", v1."BillingAddress_ZipCode", v1."BillingAddress_Country_Code", v1."BillingAddress_Country_FullName", v1."ShippingAddress_AddressLine1", v1."ShippingAddress_AddressLine2", v1."ShippingAddress_ZipCode", v1."ShippingAddress_Country_Code", v1."ShippingAddress_Country_FullName" -FROM "ValuedCustomerGroup" AS v -LEFT JOIN "ValuedCustomer" AS v0 ON v."OptionalCustomerId" = v0."Id" -INNER JOIN "ValuedCustomer" AS v1 ON v."RequiredCustomerId" = v1."Id" -WHERE v0."ShippingAddress_ZipCode" <> 7728 OR v0."ShippingAddress_ZipCode" IS NULL -"""); - } - - public override async Task Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(bool async) - { - await base.Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(async); - - AssertSql( - """ -SELECT v."Id", v."OptionalCustomerId", v."RequiredCustomerId", v1."Id", v1."Name", v1."BillingAddress_AddressLine1", v1."BillingAddress_AddressLine2", v1."BillingAddress_ZipCode", v1."BillingAddress_Country_Code", v1."BillingAddress_Country_FullName", v1."ShippingAddress_AddressLine1", v1."ShippingAddress_AddressLine2", v1."ShippingAddress_ZipCode", v1."ShippingAddress_Country_Code", v1."ShippingAddress_Country_FullName", 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 "ValuedCustomerGroup" AS v -INNER JOIN "ValuedCustomer" AS v0 ON v."RequiredCustomerId" = v0."Id" -LEFT JOIN "ValuedCustomer" AS v1 ON v."OptionalCustomerId" = v1."Id" -WHERE v0."ShippingAddress_ZipCode" <> 7728 -"""); - } - - public override async Task Project_struct_complex_type_via_optional_navigation(bool async) - { - await base.Project_struct_complex_type_via_optional_navigation(async); - - AssertSql( - """ -SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" -FROM "ValuedCustomerGroup" AS v -LEFT JOIN "ValuedCustomer" AS v0 ON v."OptionalCustomerId" = v0."Id" -"""); - } - - public override async Task Project_nullable_struct_complex_type_via_optional_navigation(bool async) - { - await base.Project_nullable_struct_complex_type_via_optional_navigation(async); - - AssertSql( - """ -SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" -FROM "ValuedCustomerGroup" AS v -LEFT JOIN "ValuedCustomer" AS v0 ON v."OptionalCustomerId" = v0."Id" -"""); - } - - public override async Task Project_struct_complex_type_via_required_navigation(bool async) - { - await base.Project_struct_complex_type_via_required_navigation(async); - - AssertSql( - """ -SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" -FROM "ValuedCustomerGroup" AS v -INNER JOIN "ValuedCustomer" AS v0 ON v."RequiredCustomerId" = v0."Id" -"""); - } - - public override async Task Load_struct_complex_type_after_subquery_on_entity_type(bool async) - { - await base.Load_struct_complex_type_after_subquery_on_entity_type(async); - - AssertSql( - """ -@p='1' - -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 -) AS v0 -"""); - } - - public override async Task Select_struct_complex_type(bool async) - { - await base.Select_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -"""); - } - - public override async Task Select_nested_struct_complex_type(bool async) - { - await base.Select_nested_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -"""); - } - - public override async Task Select_single_property_on_nested_struct_complex_type(bool async) - { - await base.Select_single_property_on_nested_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -"""); - } - - public override async Task Select_struct_complex_type_Where(bool async) - { - await base.Select_struct_complex_type_Where(async); - - AssertSql( - """ -SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -WHERE v."ShippingAddress_ZipCode" = 7728 -"""); - } - - public override async Task Select_struct_complex_type_Distinct(bool async) - { - await base.Select_struct_complex_type_Distinct(async); - - AssertSql( - """ -SELECT DISTINCT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -"""); - } - - public override async Task Struct_complex_type_equals_struct_complex_type(bool async) - { - await base.Struct_complex_type_equals_struct_complex_type(async); - - AssertSql( - """ -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" = v."BillingAddress_AddressLine1" AND (v."ShippingAddress_AddressLine2" = v."BillingAddress_AddressLine2" OR (v."ShippingAddress_AddressLine2" IS NULL AND v."BillingAddress_AddressLine2" IS NULL)) AND v."ShippingAddress_ZipCode" = v."BillingAddress_ZipCode" -"""); - } - - public override async Task Struct_complex_type_equals_constant(bool async) - { - await base.Struct_complex_type_equals_constant(async); - - AssertSql( - """ -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" = '804 S. Lakeshore Road' AND v."ShippingAddress_AddressLine2" IS NULL AND v."ShippingAddress_ZipCode" = 38654 AND v."ShippingAddress_Country_Code" = 'US' AND v."ShippingAddress_Country_FullName" = 'United States' -"""); - } - - public override async Task Struct_complex_type_equals_parameter(bool async) - { - await base.Struct_complex_type_equals_parameter(async); - - AssertSql( - """ -@entity_equality_address_AddressLine1='804 S. Lakeshore Road' -@entity_equality_address_ZipCode='38654' (Nullable = true) -@entity_equality_address_Country_Code='US' -@entity_equality_address_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_AddressLine1 AND v."ShippingAddress_AddressLine2" IS NULL AND v."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND v."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND v."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName -"""); - } - - public override async Task Subquery_over_struct_complex_type(bool async) - { - await base.Subquery_over_struct_complex_type(async); - - AssertSql(); - } - - public override async Task Contains_over_struct_complex_type(bool async) - { - await base.Contains_over_struct_complex_type(async); - - AssertSql( - """ -@entity_equality_address_AddressLine1='804 S. Lakeshore Road' -@entity_equality_address_ZipCode='38654' (Nullable = true) -@entity_equality_address_Country_Code='US' -@entity_equality_address_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_AddressLine1 AND v0."ShippingAddress_AddressLine2" IS NULL AND v0."ShippingAddress_ZipCode" = @entity_equality_address_ZipCode AND v0."ShippingAddress_Country_Code" = @entity_equality_address_Country_Code AND v0."ShippingAddress_Country_FullName" = @entity_equality_address_Country_FullName) -"""); - } - - public override async Task Concat_struct_complex_type(bool async) - { - await base.Concat_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -WHERE v."Id" = 1 -UNION ALL -SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v0 -WHERE v0."Id" = 2 -"""); - } - - public override async Task Concat_entity_type_containing_struct_complex_property(bool async) - { - await base.Concat_entity_type_containing_struct_complex_property(async); - - AssertSql( - """ -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."Id" = 1 -UNION ALL -SELECT 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 "ValuedCustomer" AS v0 -WHERE v0."Id" = 2 -"""); - } - - public override async Task Union_entity_type_containing_struct_complex_property(bool async) - { - await base.Union_entity_type_containing_struct_complex_property(async); - - AssertSql( - """ -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."Id" = 1 -UNION -SELECT 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 "ValuedCustomer" AS v0 -WHERE v0."Id" = 2 -"""); - } - - public override async Task Union_struct_complex_type(bool async) - { - await base.Union_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_AddressLine1", v."ShippingAddress_AddressLine2", v."ShippingAddress_ZipCode", v."ShippingAddress_Country_Code", v."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v -WHERE v."Id" = 1 -UNION -SELECT v0."ShippingAddress_AddressLine1", v0."ShippingAddress_AddressLine2", v0."ShippingAddress_ZipCode", v0."ShippingAddress_Country_Code", v0."ShippingAddress_Country_FullName" -FROM "ValuedCustomer" AS v0 -WHERE v0."Id" = 2 -"""); - } - - public override async Task Concat_property_in_struct_complex_type(bool async) - { - await base.Concat_property_in_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_AddressLine1" -FROM "ValuedCustomer" AS v -UNION ALL -SELECT v0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" -FROM "ValuedCustomer" AS v0 -"""); - } - - public override async Task Union_property_in_struct_complex_type(bool async) - { - await base.Union_property_in_struct_complex_type(async); - - AssertSql( - """ -SELECT v."ShippingAddress_AddressLine1" -FROM "ValuedCustomer" AS v -UNION -SELECT v0."BillingAddress_AddressLine1" AS "ShippingAddress_AddressLine1" -FROM "ValuedCustomer" AS v0 -"""); - } - - public override async Task Concat_two_different_struct_complex_type(bool async) - { - await base.Concat_two_different_struct_complex_type(async); - - AssertSql(); - } - - public override async Task Union_two_different_struct_complex_type(bool async) - { - await base.Union_two_different_struct_complex_type(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."OptionalAddress_AddressLine1", s."OptionalAddress_AddressLine2", s."OptionalAddress_Tags", s."OptionalAddress_ZipCode", s."OptionalAddress_Country_Code", s."OptionalAddress_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."OptionalAddress_AddressLine10", s."OptionalAddress_AddressLine20", s."OptionalAddress_Tags0", s."OptionalAddress_ZipCode0", s."OptionalAddress_Country_Code0", s."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c0."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c0."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c0."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c0."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c0."OptionalAddress_Country_FullName" AS "OptionalAddress_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='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."OptionalAddress_AddressLine1", s0."OptionalAddress_AddressLine2", s0."OptionalAddress_Tags", s0."OptionalAddress_ZipCode", s0."OptionalAddress_Country_Code", s0."OptionalAddress_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."OptionalAddress_AddressLine10", s0."OptionalAddress_AddressLine20", s0."OptionalAddress_Tags0", s0."OptionalAddress_ZipCode0", s0."OptionalAddress_Country_Code0", s0."OptionalAddress_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."OptionalAddress_AddressLine1", s."OptionalAddress_AddressLine2", s."OptionalAddress_Tags", s."OptionalAddress_ZipCode", s."OptionalAddress_Country_Code", s."OptionalAddress_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."OptionalAddress_AddressLine10", s."OptionalAddress_AddressLine20", s."OptionalAddress_Tags0", s."OptionalAddress_ZipCode0", s."OptionalAddress_Country_Code0", s."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c0."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c0."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c0."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c0."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c0."OptionalAddress_Country_FullName" AS "OptionalAddress_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 - ) 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='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 - ) 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='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 - ) 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='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 - ) 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='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."OptionalAddress_AddressLine1", u."OptionalAddress_AddressLine2", u."OptionalAddress_Tags", u."OptionalAddress_ZipCode", u."OptionalAddress_Country_Code", u."OptionalAddress_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."OptionalAddress_AddressLine10", u."OptionalAddress_AddressLine20", u."OptionalAddress_Tags0", u."OptionalAddress_ZipCode0", u."OptionalAddress_Country_Code0", u."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c0."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c0."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c0."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c0."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c0."OptionalAddress_Country_FullName" AS "OptionalAddress_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."OptionalAddress_AddressLine1", c1."OptionalAddress_AddressLine2", c1."OptionalAddress_Tags", c1."OptionalAddress_ZipCode", c1."OptionalAddress_Country_Code", c1."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c2."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c2."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c2."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c2."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c2."OptionalAddress_Country_FullName" AS "OptionalAddress_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 -"""); - } - - 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='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."OptionalAddress_AddressLine1", u1."OptionalAddress_AddressLine2", u1."OptionalAddress_Tags", u1."OptionalAddress_ZipCode", u1."OptionalAddress_Country_Code", u1."OptionalAddress_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."OptionalAddress_AddressLine10", u1."OptionalAddress_AddressLine20", u1."OptionalAddress_Tags0", u1."OptionalAddress_ZipCode0", u1."OptionalAddress_Country_Code0", u1."OptionalAddress_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."OptionalAddress_AddressLine1", u0."OptionalAddress_AddressLine2", u0."OptionalAddress_Tags", u0."OptionalAddress_ZipCode", u0."OptionalAddress_Country_Code", u0."OptionalAddress_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."OptionalAddress_AddressLine10", u0."OptionalAddress_AddressLine20", u0."OptionalAddress_Tags0", u0."OptionalAddress_ZipCode0", u0."OptionalAddress_Country_Code0", u0."OptionalAddress_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."OptionalAddress_AddressLine1", u."OptionalAddress_AddressLine2", u."OptionalAddress_Tags", u."OptionalAddress_ZipCode", u."OptionalAddress_Country_Code", u."OptionalAddress_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."OptionalAddress_AddressLine10", u."OptionalAddress_AddressLine20", u."OptionalAddress_Tags0", u."OptionalAddress_ZipCode0", u."OptionalAddress_Country_Code0", u."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c0."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c0."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c0."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c0."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c0."OptionalAddress_Country_FullName" AS "OptionalAddress_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."OptionalAddress_AddressLine1", c1."OptionalAddress_AddressLine2", c1."OptionalAddress_Tags", c1."OptionalAddress_ZipCode", c1."OptionalAddress_Country_Code", c1."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c2."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c2."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c2."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c2."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c2."OptionalAddress_Country_FullName" AS "OptionalAddress_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 - ) AS u0 -) AS u1 -ORDER BY u1."Id" NULLS FIRST, u1."Id0" NULLS FIRST -LIMIT @p -"""); - } - - 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='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 -"""); - } - - 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='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 - ) AS u0 -) AS u1 -ORDER BY u1."BillingAddress_ZipCode" NULLS FIRST, u1."BillingAddress_ZipCode0" NULLS FIRST -LIMIT @p -"""); - } - - 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."OptionalAddress_AddressLine1", s."OptionalAddress_AddressLine2", s."OptionalAddress_Tags", s."OptionalAddress_ZipCode", s."OptionalAddress_Country_Code", s."OptionalAddress_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."OptionalAddress_AddressLine10", s."OptionalAddress_AddressLine20", s."OptionalAddress_Tags0", s."OptionalAddress_ZipCode0", s."OptionalAddress_Country_Code0", s."OptionalAddress_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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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."OptionalAddress_AddressLine1" AS "OptionalAddress_AddressLine10", c1."OptionalAddress_AddressLine2" AS "OptionalAddress_AddressLine20", c1."OptionalAddress_Tags" AS "OptionalAddress_Tags0", c1."OptionalAddress_ZipCode" AS "OptionalAddress_ZipCode0", c1."OptionalAddress_Country_Code" AS "OptionalAddress_Country_Code0", c1."OptionalAddress_Country_FullName" AS "OptionalAddress_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."OptionalAddress_AddressLine1", c3."OptionalAddress_AddressLine2", c3."OptionalAddress_Tags", c3."OptionalAddress_ZipCode", c3."OptionalAddress_Country_Code", c3."OptionalAddress_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."OptionalAddress_AddressLine1", c2."OptionalAddress_AddressLine2", c2."OptionalAddress_Tags", c2."OptionalAddress_ZipCode", c2."OptionalAddress_Country_Code", c2."OptionalAddress_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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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='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 - ) 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='20' -@p0='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."OptionalAddress_AddressLine1", c0."OptionalAddress_AddressLine2", c0."OptionalAddress_Tags", c0."OptionalAddress_ZipCode", c0."OptionalAddress_Country_Code", c0."OptionalAddress_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."OptionalAddress_AddressLine1", c."OptionalAddress_AddressLine2", c."OptionalAddress_Tags", c."OptionalAddress_ZipCode", c."OptionalAddress_Country_Code", c."OptionalAddress_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 - ) 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."OptionalAddress_AddressLine1", c2."OptionalAddress_AddressLine2", c2."OptionalAddress_Tags", c2."OptionalAddress_ZipCode", c2."OptionalAddress_Country_Code", c2."OptionalAddress_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."OptionalAddress_AddressLine1", c1."OptionalAddress_AddressLine2", c1."OptionalAddress_Tags", c1."OptionalAddress_ZipCode", c1."OptionalAddress_Country_Code", c1."OptionalAddress_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 @p0 - ) AS c2 -) AS c4 ON c3."Id" = c4."Id" -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class ComplexTypeQueryNpgsqlFixture : ComplexTypeQueryRelationalFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/CompositeKeysQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/CompositeKeysQueryNpgsqlFixture.cs deleted file mode 100644 index a826ae7303..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/CompositeKeysQueryNpgsqlFixture.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.CompositeKeysModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class CompositeKeysQueryNpgsqlFixture : CompositeKeysQueryRelationalFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(c => c.Date).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(c => c.Date).HasColumnType("timestamp without time zone"); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/CompositeKeysQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompositeKeysQueryNpgsqlTest.cs deleted file mode 100644 index e35ded3701..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/CompositeKeysQueryNpgsqlTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class CompositeKeysQueryNpgsqlTest : CompositeKeysQueryRelationalTestBase -{ - public CompositeKeysQueryNpgsqlTest( - CompositeKeysQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/CompositeKeysSplitQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompositeKeysSplitQueryNpgsqlTest.cs deleted file mode 100644 index b53b2b6b90..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/CompositeKeysSplitQueryNpgsqlTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class CompositeKeysSplitQueryNpgsqlTest : CompositeKeysSplitQueryRelationalTestBase -{ - public CompositeKeysSplitQueryNpgsqlTest( - CompositeKeysQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs deleted file mode 100644 index 8e00b4d54c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Ef6GroupByNpgsqlTest.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class Ef6GroupByNpgsqlTest : Ef6GroupByTestBase -{ - public Ef6GroupByNpgsqlTest(Ef6GroupByNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Whats_new_2021_sample_3(bool async) - { - await base.Whats_new_2021_sample_3(async); - - AssertSql( - """ -SELECT ( - 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 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 -"""); - } - - public override async Task Whats_new_2021_sample_5(bool async) - { - await base.Whats_new_2021_sample_5(async); - - AssertSql( - """ -SELECT ( - 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 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 -"""); - } - - public override async Task Whats_new_2021_sample_6(bool async) - { - await base.Whats_new_2021_sample_6(async); - - AssertSql( - """ -SELECT ( - 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 p0."MiddleInitial" - FROM "Person" AS p0 - WHERE p0."Age" = 20 AND p."Id" = p0."Id" - LIMIT 1) NULLS FIRST -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class Ef6GroupByNpgsqlFixture : Ef6GroupByFixtureBase, ITestSqlLoggerFactory - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity().Property(o => o.OrderDate).HasColumnType("timestamp"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs deleted file mode 100644 index 381b0370e1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/EntitySplittingQueryNpgsqlTest.cs +++ /dev/null @@ -1,778 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class EntitySplittingQueryNpgsqlTest(NonSharedFixture fixture) - : EntitySplittingQueryTestBase(fixture) -{ - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - public override async Task Can_query_entity_which_is_split_in_two(bool async) - { - await base.Can_query_entity_which_is_split_in_two(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s."StringValue3", s."StringValue4" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart" AS s ON e."Id" = s."Id" -"""); - } - - public override async Task Can_query_entity_which_is_split_selecting_only_main_properties(bool async) - { - await base.Can_query_entity_which_is_split_selecting_only_main_properties(async); - - AssertSql( - """ -SELECT e."Id", e."IntValue1", e."StringValue1" -FROM "EntityOne" AS e -"""); - } - - public override async Task Can_query_entity_which_is_split_in_three(bool async) - { - await base.Can_query_entity_which_is_split_in_three(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" -"""); - } - - public override async Task Can_query_entity_which_is_split_selecting_only_part_2_properties(bool async) - { - await base.Can_query_entity_which_is_split_selecting_only_part_2_properties(async); - - AssertSql( - """ -SELECT e."Id", s."IntValue3", s."StringValue3" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart2" AS s ON e."Id" = s."Id" -"""); - } - - public override async Task Can_query_entity_which_is_split_selecting_only_part_3_properties(bool async) - { - await base.Can_query_entity_which_is_split_selecting_only_part_3_properties(async); - - AssertSql( - """ -SELECT e."Id", s."IntValue4", s."StringValue4" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" -"""); - } - - public override async Task Include_reference_to_split_entity(bool async) - { - await base.Include_reference_to_split_entity(async); - - AssertSql( - """ -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 s1 ON e."EntityOneId" = s1."Id" -"""); - } - - public override async Task Include_collection_to_split_entity(bool async) - { - await base.Include_collection_to_split_entity(async); - - AssertSql( - """ -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 s1 ON e."Id" = s1."EntityThreeId" -ORDER BY e."Id" NULLS FIRST -"""); - } - - public override async Task Include_reference_to_split_entity_including_reference(bool async) - { - await base.Include_reference_to_split_entity_including_reference(async); - - AssertSql( - """ -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 s1 ON e."EntityOneId" = s1."Id" -LEFT JOIN "EntityThree" AS e1 ON s1."EntityThreeId" = e1."Id" -"""); - } - - public override async Task Include_collection_to_split_entity_including_collection(bool async) - { - await base.Include_collection_to_split_entity_including_collection(async); - - AssertSql( - """ -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" - FROM "EntityOne" AS e0 - 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 s1 ON e."Id" = s1."EntityThreeId" -ORDER BY e."Id" NULLS FIRST, s1."Id" NULLS FIRST -"""); - } - - public override async Task Include_reference_on_split_entity(bool async) - { - await base.Include_reference_on_split_entity(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", e0."Id", e0."Name" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" -LEFT JOIN "EntityThree" AS e0 ON e."EntityThreeId" = e0."Id" -"""); - } - - public override async Task Include_collection_on_split_entity(bool async) - { - await base.Include_collection_on_split_entity(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", e0."Id", e0."EntityOneId", e0."Name" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" -LEFT JOIN "EntityTwo" AS e0 ON e."Id" = e0."EntityOneId" -ORDER BY e."Id" NULLS FIRST -"""); - } - - public override async Task Custom_projection_trim_when_multiple_tables(bool async) - { - await base.Custom_projection_trim_when_multiple_tables(async); - - AssertSql( - """ -SELECT e."IntValue1", s."IntValue3", e0."Id", e0."Name" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart2" AS s ON e."Id" = s."Id" -LEFT JOIN "EntityThree" AS e0 ON e."EntityThreeId" = e0."Id" -"""); - } - - public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_sharing(bool async) - { - await base.Normal_entity_owning_a_split_reference_with_main_fragment_sharing(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", e."IntValue3", e."IntValue4", e."StringValue1", e."StringValue2", e."StringValue3", e."StringValue4", e."OwnedReference_Id", e."OwnedReference_OwnedIntValue1", e."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", e."OwnedReference_OwnedStringValue1", e."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "EntityOne" AS e -LEFT JOIN "OwnedReferenceExtras2" AS o ON e."Id" = o."EntityOneId" -LEFT JOIN "OwnedReferenceExtras1" AS o0 ON e."Id" = o0."EntityOneId" -"""); - } - - public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_sharing_custom_projection(bool async) - { - await base.Normal_entity_owning_a_split_reference_with_main_fragment_sharing_custom_projection(async); - - AssertSql( - """ -SELECT e."Id", CASE - WHEN e."OwnedReference_Id" IS NOT NULL AND e."OwnedReference_OwnedIntValue1" IS NOT NULL AND e."OwnedReference_OwnedIntValue2" IS NOT NULL AND o0."OwnedIntValue3" IS NOT NULL AND o."OwnedIntValue4" IS NOT NULL THEN o."OwnedIntValue4" -END AS "OwnedIntValue4", CASE - WHEN e."OwnedReference_Id" IS NOT NULL AND e."OwnedReference_OwnedIntValue1" IS NOT NULL AND e."OwnedReference_OwnedIntValue2" IS NOT NULL AND o0."OwnedIntValue3" IS NOT NULL AND o."OwnedIntValue4" IS NOT NULL THEN o."OwnedStringValue4" -END AS "OwnedStringValue4" -FROM "EntityOnes" AS e -LEFT JOIN "OwnedReferenceExtras2" AS o ON e."Id" = o."EntityOneId" -LEFT JOIN "OwnedReferenceExtras1" AS o0 ON e."Id" = o0."EntityOneId" -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing(bool async) - { - await base.Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing_custom_projection(bool async) - { - await base.Normal_entity_owning_a_split_reference_with_main_fragment_not_sharing_custom_projection(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Normal_entity_owning_a_split_collection(bool async) - { - await base.Normal_entity_owning_a_split_collection(async); - - AssertSql(); - } - - public override async Task Normal_entity_owning_a_split_reference_with_main_fragment_sharing_multiple_level(bool async) - { - await base.Normal_entity_owning_a_split_reference_with_main_fragment_sharing_multiple_level(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", e."IntValue3", e."IntValue4", e."StringValue1", e."StringValue2", e."StringValue3", e."StringValue4", e."OwnedReference_Id", e."OwnedReference_OwnedIntValue1", e."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", e."OwnedReference_OwnedStringValue1", e."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4", e."OwnedReference_OwnedNestedReference_Id", e."OwnedReference_OwnedNestedReference_OwnedNestedIntValue1", e."OwnedReference_OwnedNestedReference_OwnedNestedIntValue2", o2."OwnedNestedIntValue3", o1."OwnedNestedIntValue4", e."OwnedReference_OwnedNestedReference_OwnedNestedStringValue1", e."OwnedReference_OwnedNestedReference_OwnedNestedStringValue2", o2."OwnedNestedStringValue3", o1."OwnedNestedStringValue4" -FROM "EntityOnes" AS e -LEFT JOIN "OwnedReferenceExtras2" AS o ON e."Id" = o."EntityOneId" -LEFT JOIN "OwnedReferenceExtras1" AS o0 ON e."Id" = o0."EntityOneId" -LEFT JOIN "OwnedNestedReferenceExtras2" AS o1 ON e."Id" = o1."OwnedReferenceEntityOneId" -LEFT JOIN "OwnedNestedReferenceExtras1" AS o2 ON e."Id" = o2."OwnedReferenceEntityOneId" -"""); - } - - public override async Task Split_entity_owning_a_reference(bool async) - { - await base.Split_entity_owning_a_reference(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", e."OwnedReference_Id", e."OwnedReference_OwnedIntValue1", e."OwnedReference_OwnedIntValue2", e."OwnedReference_OwnedIntValue3", e."OwnedReference_OwnedIntValue4", e."OwnedReference_OwnedStringValue1", e."OwnedReference_OwnedStringValue2", e."OwnedReference_OwnedStringValue3", e."OwnedReference_OwnedStringValue4" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" -"""); - } - - public override async Task Split_entity_owning_a_collection(bool async) - { - await base.Split_entity_owning_a_collection(async); - - AssertSql( - """ -SELECT e."Id", e."EntityThreeId", e."IntValue1", e."IntValue2", s0."IntValue3", s."IntValue4", e."StringValue1", e."StringValue2", s0."StringValue3", s."StringValue4", o."EntityOneId", o."Id", o."OwnedIntValue1", o."OwnedIntValue2", o."OwnedIntValue3", o."OwnedIntValue4", o."OwnedStringValue1", o."OwnedStringValue2", o."OwnedStringValue3", o."OwnedStringValue4" -FROM "EntityOne" AS e -INNER JOIN "SplitEntityOnePart3" AS s ON e."Id" = s."Id" -INNER JOIN "SplitEntityOnePart2" AS s0 ON e."Id" = s0."Id" -LEFT JOIN "OwnedCollection" AS o ON e."Id" = o."EntityOneId" -ORDER BY e."Id" NULLS FIRST, o."EntityOneId" NULLS FIRST -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Split_entity_owning_a_split_reference_without_table_sharing(bool async) - { - await base.Split_entity_owning_a_split_reference_without_table_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Split_entity_owning_a_split_collection(bool async) - { - await base.Split_entity_owning_a_split_collection(async); - - AssertSql(); - } - - public override async Task Split_entity_owning_a_split_reference_with_table_sharing_1(bool async) - { - await base.Split_entity_owning_a_split_reference_with_table_sharing_1(async); - - AssertSql( - """ -SELECT s."Id", s."EntityThreeId", s."IntValue1", s."IntValue2", s1."IntValue3", s0."IntValue4", s."StringValue1", s."StringValue2", s1."StringValue3", s0."StringValue4", s."OwnedReference_Id", s."OwnedReference_OwnedIntValue1", s."OwnedReference_OwnedIntValue2", s1."OwnedReference_OwnedIntValue3", s0."OwnedReference_OwnedIntValue4", s."OwnedReference_OwnedStringValue1", s."OwnedReference_OwnedStringValue2", s1."OwnedReference_OwnedStringValue3", s0."OwnedReference_OwnedStringValue4" -FROM "SplitEntityOnePart1" AS s -INNER JOIN "SplitEntityOnePart3" AS s0 ON s."Id" = s0."Id" -INNER JOIN "SplitEntityOnePart2" AS s1 ON s."Id" = s1."Id" -"""); - } - - public override async Task Split_entity_owning_a_split_reference_with_table_sharing_4(bool async) - { - await base.Split_entity_owning_a_split_reference_with_table_sharing_4(async); - - AssertSql( - """ -SELECT s."Id", s."EntityThreeId", s."IntValue1", s."IntValue2", s1."IntValue3", s0."IntValue4", s."StringValue1", s."StringValue2", s1."StringValue3", s0."StringValue4", s."OwnedReference_Id", s."OwnedReference_OwnedIntValue1", s."OwnedReference_OwnedIntValue2", s1."OwnedReference_OwnedIntValue3", o."OwnedIntValue4", s."OwnedReference_OwnedStringValue1", s."OwnedReference_OwnedStringValue2", s1."OwnedReference_OwnedStringValue3", o."OwnedStringValue4" -FROM "SplitEntityOnePart1" AS s -INNER JOIN "SplitEntityOnePart3" AS s0 ON s."Id" = s0."Id" -INNER JOIN "SplitEntityOnePart2" AS s1 ON s."Id" = s1."Id" -LEFT JOIN "OwnedReferencePart3" AS o ON s."Id" = o."EntityOneId" -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Split_entity_owning_a_split_reference_with_table_sharing_6(bool async) - { - await base.Split_entity_owning_a_split_reference_with_table_sharing_6(async); - - AssertSql( - """ -SELECT s."Id", s."EntityThreeId", s."IntValue1", s."IntValue2", s1."IntValue3", s0."IntValue4", s."StringValue1", s."StringValue2", s1."StringValue3", s0."StringValue4", s1."Id", s1."OwnedReference_Id", s1."OwnedReference_OwnedIntValue1", s1."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", s1."OwnedReference_OwnedStringValue1", s1."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "SplitEntityOnePart1" AS s -INNER JOIN "SplitEntityOnePart3" AS s0 ON s."Id" = s0."Id" -INNER JOIN "SplitEntityOnePart2" AS s1 ON s."Id" = s1."Id" -LEFT JOIN "OwnedReferencePart3" AS o ON s1."Id" = o."EntityOneId" -LEFT JOIN "OwnedReferencePart2" AS o0 ON s1."Id" = o0."EntityOneId" -"""); - } - - public override async Task Tph_entity_owning_a_split_reference_on_base_with_table_sharing(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_base_with_table_sharing(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", b."Discriminator", b."MiddleValue", b."SiblingValue", b."LeafValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" -"""); - } - - public override async Task Tpt_entity_owning_a_split_reference_on_base_with_table_sharing(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_base_with_table_sharing(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", m."MiddleValue", s."SiblingValue", l."LeafValue", CASE - WHEN l."Id" IS NOT NULL THEN 'LeafEntity' - WHEN s."Id" IS NOT NULL THEN 'SiblingEntity' - WHEN m."Id" IS NOT NULL THEN 'MiddleEntity' -END AS "Discriminator", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -LEFT JOIN "MiddleEntity" AS m ON b."Id" = m."Id" -LEFT JOIN "SiblingEntity" AS s ON b."Id" = s."Id" -LEFT JOIN "LeafEntity" AS l ON b."Id" = l."Id" -LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" -"""); - } - - public override async Task Tph_entity_owning_a_split_reference_on_middle_with_table_sharing(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_middle_with_table_sharing(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", b."Discriminator", b."MiddleValue", b."SiblingValue", b."LeafValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."MiddleEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."MiddleEntityId" -"""); - } - - public override async Task Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", m."MiddleValue", s."SiblingValue", l."LeafValue", CASE - WHEN l."Id" IS NOT NULL THEN 'LeafEntity' - WHEN s."Id" IS NOT NULL THEN 'SiblingEntity' - WHEN m."Id" IS NOT NULL THEN 'MiddleEntity' -END AS "Discriminator", m."Id", m."OwnedReference_Id", m."OwnedReference_OwnedIntValue1", m."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", m."OwnedReference_OwnedStringValue1", m."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -LEFT JOIN "MiddleEntity" AS m ON b."Id" = m."Id" -LEFT JOIN "SiblingEntity" AS s ON b."Id" = s."Id" -LEFT JOIN "LeafEntity" AS l ON b."Id" = l."Id" -LEFT JOIN "OwnedReferencePart4" AS o ON m."Id" = o."MiddleEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON m."Id" = o0."MiddleEntityId" -"""); - } - - public override async Task Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", b."Discriminator", b."MiddleValue", b."SiblingValue", b."LeafValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."LeafEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."LeafEntityId" -"""); - } - - public override async Task Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", m."MiddleValue", s."SiblingValue", l."LeafValue", CASE - WHEN l."Id" IS NOT NULL THEN 'LeafEntity' - WHEN s."Id" IS NOT NULL THEN 'SiblingEntity' - WHEN m."Id" IS NOT NULL THEN 'MiddleEntity' -END AS "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" -FROM "BaseEntity" AS b -LEFT JOIN "MiddleEntity" AS m ON b."Id" = m."Id" -LEFT JOIN "SiblingEntity" AS s ON b."Id" = s."Id" -LEFT JOIN "LeafEntity" AS l ON b."Id" = l."Id" -LEFT JOIN "OwnedReferencePart4" AS o ON l."Id" = o."LeafEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON l."Id" = o0."LeafEntityId" -"""); - } - - public override async Task Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing(bool async) - { - await base.Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing(async); - - AssertSql( - """ -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 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" - FROM "MiddleEntity" AS m - 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 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" -"""); - } - - public override async Task Tph_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", b."Discriminator", b."SiblingValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" -WHERE b."Discriminator" = 'SiblingEntity' -"""); - } - - public override async Task Tpt_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_base_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", s."SiblingValue", b."OwnedReference_Id", b."OwnedReference_OwnedIntValue1", b."OwnedReference_OwnedIntValue2", o0."OwnedIntValue3", o."OwnedIntValue4", b."OwnedReference_OwnedStringValue1", b."OwnedReference_OwnedStringValue2", o0."OwnedStringValue3", o."OwnedStringValue4" -FROM "BaseEntity" AS b -INNER JOIN "SiblingEntity" AS s ON b."Id" = s."Id" -LEFT JOIN "OwnedReferencePart4" AS o ON b."Id" = o."BaseEntityId" -LEFT JOIN "OwnedReferencePart3" AS o0 ON b."Id" = o0."BaseEntityId" -"""); - } - - public override async Task Tph_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", b."Discriminator", b."SiblingValue" -FROM "BaseEntity" AS b -WHERE b."Discriminator" = 'SiblingEntity' -"""); - } - - public override async Task Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_middle_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", s."SiblingValue" -FROM "BaseEntity" AS b -INNER JOIN "SiblingEntity" AS s ON b."Id" = s."Id" -"""); - } - - public override async Task Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", b."Discriminator", b."SiblingValue" -FROM "BaseEntity" AS b -WHERE b."Discriminator" = 'SiblingEntity' -"""); - } - - public override async Task Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT b."Id", b."BaseValue", s."SiblingValue" -FROM "BaseEntity" AS b -INNER JOIN "SiblingEntity" AS s ON b."Id" = s."Id" -"""); - } - - public override async Task Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(bool async) - { - await base.Tpc_entity_owning_a_split_reference_on_leaf_with_table_sharing_querying_sibling(async); - - AssertSql( - """ -SELECT s."Id", s."BaseValue", s."SiblingValue" -FROM "SiblingEntity" AS s -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tph_entity_owning_a_split_reference_on_base_without_table_sharing(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_base_without_table_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpt_entity_owning_a_split_reference_on_base_without_table_sharing(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_base_without_table_sharing(async); - - AssertSql(); - } - - public override async Task Tpc_entity_owning_a_split_reference_on_base_without_table_sharing(bool async) - { - await base.Tpc_entity_owning_a_split_reference_on_base_without_table_sharing(async); - - AssertSql( - """ -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 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" - FROM "MiddleEntity" AS m - 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" - FROM "LeafEntity" AS l -) 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" -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tph_entity_owning_a_split_reference_on_middle_without_table_sharing(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_middle_without_table_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpt_entity_owning_a_split_reference_on_middle_without_table_sharing(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_middle_without_table_sharing(async); - - AssertSql(); - } - - public override async Task Tpc_entity_owning_a_split_reference_on_middle_without_table_sharing(bool async) - { - await base.Tpc_entity_owning_a_split_reference_on_middle_without_table_sharing(async); - - AssertSql( - """ -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 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" - FROM "MiddleEntity" AS m - 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" - FROM "LeafEntity" AS l -) 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" -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tph_entity_owning_a_split_reference_on_leaf_without_table_sharing(bool async) - { - await base.Tph_entity_owning_a_split_reference_on_leaf_without_table_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpt_entity_owning_a_split_reference_on_leaf_without_table_sharing(bool async) - { - await base.Tpt_entity_owning_a_split_reference_on_leaf_without_table_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpc_entity_owning_a_split_reference_on_leaf_without_table_sharing(bool async) - { - await base.Tpc_entity_owning_a_split_reference_on_leaf_without_table_sharing(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tph_entity_owning_a_split_collection_on_base(bool async) - { - await base.Tph_entity_owning_a_split_collection_on_base(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpt_entity_owning_a_split_collection_on_base(bool async) - { - await base.Tpt_entity_owning_a_split_collection_on_base(async); - - AssertSql(); - } - - public override async Task Tpc_entity_owning_a_split_collection_on_base(bool async) - { - await base.Tpc_entity_owning_a_split_collection_on_base(async); - - AssertSql( - """ -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 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" - FROM "MiddleEntity" AS m - 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" - FROM "LeafEntity" AS l -) 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 s0 ON u."Id" = s0."BaseEntityId" -ORDER BY u."Id" NULLS FIRST, s0."BaseEntityId" NULLS FIRST -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tph_entity_owning_a_split_collection_on_middle(bool async) - { - await base.Tph_entity_owning_a_split_collection_on_middle(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpt_entity_owning_a_split_collection_on_middle(bool async) - { - await base.Tpt_entity_owning_a_split_collection_on_middle(async); - - AssertSql(); - } - - public override async Task Tpc_entity_owning_a_split_collection_on_middle(bool async) - { - await base.Tpc_entity_owning_a_split_collection_on_middle(async); - - AssertSql( - """ -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 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" - FROM "MiddleEntity" AS m - 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 l."Id", l."BaseValue", l."MiddleValue", NULL AS "SiblingValue", l."LeafValue", 'LeafEntity' AS "Discriminator" - FROM "LeafEntity" AS l -) 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 s0 ON u."Id" = s0."MiddleEntityId" -ORDER BY u."Id" NULLS FIRST, s0."MiddleEntityId" NULLS FIRST -"""); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tph_entity_owning_a_split_collection_on_leaf(bool async) - { - await base.Tph_entity_owning_a_split_collection_on_leaf(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpt_entity_owning_a_split_collection_on_leaf(bool async) - { - await base.Tpt_entity_owning_a_split_collection_on_leaf(async); - - AssertSql(); - } - - [ConditionalTheory(Skip = "Issue29075")] - public override async Task Tpc_entity_owning_a_split_collection_on_leaf(bool async) - { - await base.Tpc_entity_owning_a_split_collection_on_leaf(async); - - AssertSql(); - } - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs deleted file mode 100644 index 7a8643273b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/FieldsOnlyLoadNpgsqlTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class FieldsOnlyLoadNpgsqlTest(FieldsOnlyLoadNpgsqlTest.FieldsOnlyLoadNpgsqlFixture fixture) - : FieldsOnlyLoadTestBase(fixture) -{ - public class FieldsOnlyLoadNpgsqlFixture : FieldsOnlyLoadFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/FiltersInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FiltersInheritanceQueryNpgsqlTest.cs deleted file mode 100644 index df7a258761..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/FiltersInheritanceQueryNpgsqlTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class FiltersInheritanceQueryNpgsqlTest : FiltersInheritanceQueryTestBase -{ - // ReSharper disable once UnusedParameter.Local - public FiltersInheritanceQueryNpgsqlTest(TPHFiltersInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs deleted file mode 100644 index 1eaa9ea668..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/FromSqlQueryNpgsqlTest.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Data.Common; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class FromSqlQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) - : FromSqlQueryTestBase>(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); - - [ConditionalTheory(Skip = "https://github.com/aspnet/EntityFramework/issues/{6563,20364}")] - public override Task Bad_data_error_handling_invalid_cast_projection(bool async) - => base.Bad_data_error_handling_invalid_cast_projection(async); - - // 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) - { - // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match - // the database data. - var city = "London"; - var startDate = new NpgsqlParameter { Value = new DateTime(1997, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - var endDate = new NpgsqlParameter { Value = new DateTime(1998, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - await using var context = CreateContext(); - var query - = from c in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( - NormalizeDelimitersInInterpolatedString( - $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) - where c.CustomerID == o.CustomerID - select new { c, o }; - - var actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Equal(25, actual.Length); - - city = "Berlin"; - startDate = new NpgsqlParameter { Value = new DateTime(1998, 4, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - endDate = new NpgsqlParameter { Value = new DateTime(1998, 5, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - query - = (from c in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( - NormalizeDelimitersInInterpolatedString( - $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) - where c.CustomerID == o.CustomerID - select new { c, o }); - - actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Single(actual); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public override async Task FromSql_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(bool async) - { - // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match - // the database data. - var city = "London"; - var startDate = new NpgsqlParameter { Value = new DateTime(1997, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - var endDate = new NpgsqlParameter { Value = new DateTime(1998, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - await using var context = CreateContext(); - var query - = from c in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( - NormalizeDelimitersInInterpolatedString( - $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) - where c.CustomerID == o.CustomerID - select new { c, o }; - - var actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Equal(25, actual.Length); - - city = "Berlin"; - startDate = new NpgsqlParameter { Value = new DateTime(1998, 4, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - endDate = new NpgsqlParameter { Value = new DateTime(1998, 5, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - query - = (from c in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlInterpolated( - NormalizeDelimitersInInterpolatedString( - $"SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {startDate} AND {endDate}")) - where c.CustomerID == o.CustomerID - select new { c, o }); - - actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Single(actual); - } - - public override async Task FromSqlRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) - { - // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match - // the database data. - var city = "London"; - var startDate = new NpgsqlParameter { Value = new DateTime(1997, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - var endDate = new NpgsqlParameter { Value = new DateTime(1998, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - await using var context = CreateContext(); - var query = from c in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), - startDate, - endDate) - where c.CustomerID == o.CustomerID - select new { c, o }; - - var actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Equal(25, actual.Length); - - city = "Berlin"; - startDate = new NpgsqlParameter { Value = new DateTime(1998, 4, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - endDate = new NpgsqlParameter { Value = new DateTime(1998, 5, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - query = (from c in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Customers] WHERE [City] = {0}"), city) - from o in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), - startDate, - endDate) - where c.CustomerID == o.CustomerID - select new { c, o }); - - actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Single(actual); - } - - public override async Task FromSqlRaw_queryable_multiple_composed_with_closure_parameters(bool async) - { - // We default to mapping DateTime to 'timestamp with time zone', but here we need to send `timestamp without time zone` to match - // the database data. - var startDate = new NpgsqlParameter { Value = new DateTime(1997, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - var endDate = new NpgsqlParameter { Value = new DateTime(1998, 1, 1), NpgsqlDbType = NpgsqlDbType.Timestamp }; - - await using var context = CreateContext(); - var query = from c in context.Set().FromSqlRaw(NormalizeDelimitersInRawString("SELECT * FROM [Customers]")) - from o in context.Set().FromSqlRaw( - NormalizeDelimitersInRawString("SELECT * FROM [Orders] WHERE [OrderDate] BETWEEN {0} AND {1}"), - startDate, - endDate) - where c.CustomerID == o.CustomerID - select new { c, o }; - - var actual = async - ? await query.ToArrayAsync() - : query.ToArray(); - - Assert.Equal(411, actual.Length); - } - - protected override DbParameter CreateDbParameter(string name, object value) - => new NpgsqlParameter { ParameterName = name, Value = value }; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs deleted file mode 100644 index e54ea82d13..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs +++ /dev/null @@ -1,1051 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -#pragma warning disable CS0618 // NpgsqlTsVector.Parse is obsolete - -public class FullTextSearchDbFunctionsNpgsqlTest : IClassFixture> -{ - protected NorthwindQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public FullTextSearchDbFunctionsNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [Fact] - public void TsVectorParse_converted_to_cast() - { - using var context = CreateContext(); - var tsvector = context.Customers.Select(c => NpgsqlTsVector.Parse("a b")).First(); - - Assert.NotNull(tsvector); - AssertSql( - """ -SELECT 'a b'::tsvector -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ArrayToTsVector_constants() - { - using var context = CreateContext(); - var tsvector = context.Customers.Select(c => EF.Functions.ArrayToTsVector(new[] { "b", "c", "d" })) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("b c d").ToString(), tsvector.ToString()); - AssertSql( - """ -SELECT array_to_tsvector(ARRAY['b','c','d']::text[]) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ArrayToTsVector_columns() - { - using var context = CreateContext(); - var tsvector = context.Customers - .OrderBy(c => c.CustomerID) - .Select(c => EF.Functions.ArrayToTsVector(new[] { c.CompanyName, c.Address })) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("'Alfreds Futterkiste' 'Obere Str. 57'").ToString(), tsvector.ToString()); - AssertSql( - """ -SELECT array_to_tsvector(ARRAY[c."CompanyName",c."Address"]::character varying(60)[]) -FROM "Customers" AS c -ORDER BY c."CustomerID" NULLS FIRST -LIMIT 1 -"""); - } - - [Fact] - public void ToTsVector() - { - using var context = CreateContext(); - var tsvector = context.Customers.Select(c => EF.Functions.ToTsVector(c.CompanyName)).First(); - - Assert.NotNull(tsvector); - AssertSql( - """ -SELECT to_tsvector(c."CompanyName") -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToTsVector_with_constant_config() - { - using var context = CreateContext(); - var tsvector = context.Customers.Select(c => EF.Functions.ToTsVector("english", c.CompanyName)).First(); - - Assert.NotNull(tsvector); - AssertSql( - """ -SELECT to_tsvector('english', c."CompanyName") -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToTsVector_with_parameter_config() - { - using var context = CreateContext(); - var config = "english"; - var tsvector = context.Customers.Select(c => EF.Functions.ToTsVector(config, c.CompanyName)).First(); - - Assert.NotNull(tsvector); - AssertSql( - """ -SELECT to_tsvector('english', c."CompanyName") -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsQueryParse_converted_to_cast() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => NpgsqlTsQuery.Parse("a & b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT 'a & b'::tsquery -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void PlainToTsQuery() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.PlainToTsQuery("a")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT plainto_tsquery('a') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void PlainToTsQuery_with_constant_config() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.PlainToTsQuery("english", "a")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT plainto_tsquery('english', 'a') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void PlainToTsQuery_with_parameter_config() - { - using var context = CreateContext(); - var config = "english"; - var tsquery = context.Customers.Select(c => EF.Functions.PlainToTsQuery(config, "a")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT plainto_tsquery('english', 'a') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void PhraseToTsQuery() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.PhraseToTsQuery("a b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT phraseto_tsquery('a b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void PhraseToTsQuery_with_constant_config() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.PhraseToTsQuery("english", "a b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT phraseto_tsquery('english', 'a b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void PhraseToTsQuery_with_parameter_config() - { - using var context = CreateContext(); - var config = "english"; - var tsquery = context.Customers.Select(c => EF.Functions.PhraseToTsQuery(config, "a b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT phraseto_tsquery('english', 'a b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToTsQuery() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.ToTsQuery("a & b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT to_tsquery('a & b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToTsQuery_with_constant_config() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.ToTsQuery("english", "a & b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT to_tsquery('english', 'a & b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToTsQuery_with_parameter_config() - { - using var context = CreateContext(); - var config = "english"; - var tsquery = context.Customers.Select(c => EF.Functions.ToTsQuery(config, "a & b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT to_tsquery('english', 'a & b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(11, 0)] - public void WebSearchToTsQuery() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.WebSearchToTsQuery("a OR b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT websearch_to_tsquery('a OR b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(11, 0)] - public void WebSearchToTsQuery_with_constant_config() - { - using var context = CreateContext(); - var tsquery = context.Customers.Select(c => EF.Functions.WebSearchToTsQuery("english", "a OR b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT websearch_to_tsquery('english', 'a OR b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(11, 0)] - public void WebSearchToTsQuery_with_parameter_config() - { - using var context = CreateContext(); - var config = "english"; - var tsquery = context.Customers.Select(c => EF.Functions.WebSearchToTsQuery(config, "a OR b")).First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT websearch_to_tsquery('english', 'a OR b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsQuery_and() - { - using var context = CreateContext(); - var tsquery = context.Customers - .Select(c => EF.Functions.ToTsQuery("a & b").And(EF.Functions.ToTsQuery("c & d"))) - .First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT to_tsquery('a & b') && to_tsquery('c & d') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsQuery_or() - { - using var context = CreateContext(); - var tsquery = context.Customers - .Select(c => EF.Functions.ToTsQuery("a & b").Or(EF.Functions.ToTsQuery("c & d"))) - .First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT to_tsquery('a & b') || to_tsquery('c & d') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsQuery_ToNegative() - { - using var context = CreateContext(); - var tsquery = context.Customers - .Select(c => EF.Functions.ToTsQuery("a & b").ToNegative()) - .First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT !!to_tsquery('a & b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsQuery_Contains() - { - using var context = CreateContext(); - var result = context.Customers - .Select(c => EF.Functions.ToTsQuery("a & b").Contains(EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.True(result); - AssertSql( - """ -SELECT to_tsquery('a & b') @> to_tsquery('b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsQuery_IsContainedIn() - { - using var context = CreateContext(); - var result = context.Customers - .Select(c => EF.Functions.ToTsQuery("b").IsContainedIn(EF.Functions.ToTsQuery("a & b"))) - .First(); - - Assert.True(result); - AssertSql( - """ -SELECT to_tsquery('b') <@ to_tsquery('a & b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetNodeCount() - { - using var context = CreateContext(); - var nodeCount = context.Customers - .Select(c => EF.Functions.ToTsQuery("b").GetNodeCount()) - .First(); - - Assert.Equal(1, nodeCount); - AssertSql( - """ -SELECT numnode(to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetQueryTree() - { - using var context = CreateContext(); - var queryTree = context.Customers - .Select(c => EF.Functions.ToTsQuery("b").GetQueryTree()) - .First(); - - Assert.NotEmpty(queryTree); - AssertSql( - """ -SELECT querytree(to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetResultHeadline() - { - using var context = CreateContext(); - var headline = context.Customers - .Select(c => EF.Functions.ToTsQuery("b").GetResultHeadline("a b c")) - .First(); - - Assert.NotEmpty(headline); - AssertSql( - """ -SELECT ts_headline('a b c', to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetResultHeadline_with_options() - { - using var context = CreateContext(); - var headline = context.Customers - .Select(c => EF.Functions.ToTsQuery("b").GetResultHeadline("a b c", "MinWords=1, MaxWords=2")) - .First(); - - Assert.NotEmpty(headline); - AssertSql( - """ -SELECT ts_headline('a b c', to_tsquery('b'), 'MinWords=1, MaxWords=2') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetResultHeadline_with_constant_config_and_options() - { - using var context = CreateContext(); - var headline = context.Customers - .Select( - c => EF.Functions.ToTsQuery("b").GetResultHeadline( - "english", - "a b c", - "MinWords=1, MaxWords=2")) - .First(); - - Assert.NotEmpty(headline); - AssertSql( - """ -SELECT ts_headline('english', 'a b c', to_tsquery('b'), 'MinWords=1, MaxWords=2') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetResultHeadline_with_parameter_config_and_options() - { - using var context = CreateContext(); - var config = "english"; - var headline = context.Customers - .Select( - c => EF.Functions.ToTsQuery("b").GetResultHeadline( - config, - "a b c", - "MinWords=1, MaxWords=2")) - .First(); - - Assert.NotEmpty(headline); - AssertSql( - """ -@config='english' - -SELECT ts_headline(@config::regconfig, 'a b c', to_tsquery('b'), 'MinWords=1, MaxWords=2') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Rewrite() - { - using var context = CreateContext(); - var rewritten = context.Customers - .Select( - c => EF.Functions.ToTsQuery("a & b").Rewrite( - EF.Functions.ToTsQuery("b"), - EF.Functions.ToTsQuery("c"))) - .First(); - - Assert.NotNull(rewritten); - AssertSql( - """ -SELECT ts_rewrite(to_tsquery('a & b'), to_tsquery('b'), to_tsquery('c')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Rewrite_with_select() - { - using var context = CreateContext(); - var rewritten = context.Customers - .Select( - c => EF.Functions.ToTsQuery("a & b").Rewrite( - """SELECT 'a'::tsquery, 'c'::tsquery""")) - .First(); - - Assert.NotNull(rewritten); - AssertSql( - """ -SELECT ts_rewrite(to_tsquery('a & b'), 'SELECT ''a''::tsquery, ''c''::tsquery') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToPhrase() - { - using var context = CreateContext(); - var tsquery = context.Customers - .Select(c => EF.Functions.ToTsQuery("a").ToPhrase(EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT tsquery_phrase(to_tsquery('a'), to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToPhrase_with_distance() - { - using var context = CreateContext(); - var tsquery = context.Customers - .Select(c => EF.Functions.ToTsQuery("a").ToPhrase(EF.Functions.ToTsQuery("b"), 10)) - .First(); - - Assert.NotNull(tsquery); - AssertSql( - """ -SELECT tsquery_phrase(to_tsquery('a'), to_tsquery('b'), 10) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Matches_with_string() - { - using var context = CreateContext(); - var query = "b"; - var result = context.Customers - .Select(c => EF.Functions.ToTsVector("a").Matches(query)) - .First(); - - Assert.False(result); - AssertSql( - """ -@query='b' - -SELECT to_tsvector('a') @@ plainto_tsquery(@query) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Matches_with_TsQuery() - { - using var context = CreateContext(); - var result = context.Customers - .Select(c => EF.Functions.ToTsVector("a").Matches(EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.False(result); - AssertSql( - """ -SELECT to_tsvector('a') @@ to_tsquery('b') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void TsVector_Concat() - { - using var context = CreateContext(); - var tsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("b").Concat(EF.Functions.ToTsVector("c"))) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("b:1 c:2").ToString(), tsVector.ToString()); - AssertSql( - """ -SELECT to_tsvector('b') || to_tsvector('c') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void SetWeight_with_enum() - { - using var context = CreateContext(); - var weightedTsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("a").SetWeight(NpgsqlTsVector.Lexeme.Weight.A)) - .First(); - - Assert.NotNull(weightedTsVector); - AssertSql( - """ -SELECT setweight(to_tsvector('a'), 'A') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void SetWeight_with_enum_and_lexemes() - { - using var context = CreateContext(); - var weightedTsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("a").SetWeight(NpgsqlTsVector.Lexeme.Weight.A, new[] { "a" })) - .First(); - - Assert.NotNull(weightedTsVector); - AssertSql( - """ -SELECT setweight(to_tsvector('a'), 'A', ARRAY['a']::text[]) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void SetWeight_with_char() - { - using var context = CreateContext(); - var weightedTsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("a").SetWeight('A')) - .First(); - - Assert.NotNull(weightedTsVector); - AssertSql( - """ -SELECT setweight(to_tsvector('a'), 'A') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void SetWeight_with_char_and_lexemes() - { - using var context = CreateContext(); - var weightedTsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("a").SetWeight('A', new[] { "a" })) - .First(); - - Assert.NotNull(weightedTsVector); - AssertSql( - """ -SELECT setweight(to_tsvector('a'), 'A', ARRAY['a']::text[]) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Delete_with_single_lexeme() - { - using var context = CreateContext(); - var tsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("b c").Delete("c")) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("b:1").ToString(), tsVector.ToString()); - AssertSql( - """ -SELECT ts_delete(to_tsvector('b c'), 'c') -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Delete_with_multiple_lexemes() - { - using var context = CreateContext(); - var tsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("b c d").Delete(new[] { "c", "d" })) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("b:1").ToString(), tsVector.ToString()); - AssertSql( - """ -SELECT ts_delete(to_tsvector('b c d'), ARRAY['c','d']::text[]) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact(Skip = "Need to reimplement with \"char\"[]")] - public void Filter() - { - using var context = CreateContext(); - var tsVector = context.Customers - .Select(c => NpgsqlTsVector.Parse("b:1A c:2B d:3C").Filter(new[] { 'B', 'C' })) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("c:2B d:3C").ToString(), tsVector.ToString()); - AssertSql( - """ -SELECT ts_filter(CAST('b:1A c:2B d:3C' AS tsvector), CAST(ARRAY['B','C']::character(1)[] AS "char"[])) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void GetLength() - { - using var context = CreateContext(); - var length = context.Customers - .Select(c => EF.Functions.ToTsVector("c").GetLength()) - .First(); - - Assert.Equal(1, length); - AssertSql( - """ -SELECT length(to_tsvector('c')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void ToStripped() - { - using var context = CreateContext(); - var strippedTsVector = context.Customers - .Select(c => EF.Functions.ToTsVector("c:A").ToStripped()) - .First(); - - Assert.Equal(NpgsqlTsVector.Parse("c").ToString(), strippedTsVector.ToString()); - AssertSql( - """ -SELECT strip(to_tsvector('c:A')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Rank() - { - using var context = CreateContext(); - var rank = context.Customers - .Select(c => EF.Functions.ToTsVector("a b c").Rank(EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank(to_tsvector('a b c'), to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Rank_with_normalization() - { - using var context = CreateContext(); - var rank = context.Customers - .Select( - c => EF.Functions.ToTsVector("a b c").Rank( - EF.Functions.ToTsQuery("b"), - NpgsqlTsRankingNormalization.DivideByLength)) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank(to_tsvector('a b c'), to_tsquery('b'), 2) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Rank_with_weights() - { - using var context = CreateContext(); - var rank = context.Customers - .Select( - c => EF.Functions.ToTsVector("a b c").Rank( - new float[] { 1, 1, 1, 1 }, - EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Rank_with_weights_and_normalization() - { - using var context = CreateContext(); - var rank = context.Customers - .Select( - c => EF.Functions.ToTsVector("a b c").Rank( - new float[] { 1, 1, 1, 1 }, - EF.Functions.ToTsQuery("b"), - NpgsqlTsRankingNormalization.DivideByLength)) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b'), 2) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void RankCoverDensity() - { - using var context = CreateContext(); - var rank = context.Customers - .Select(c => EF.Functions.ToTsVector("a b c").RankCoverDensity(EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank_cd(to_tsvector('a b c'), to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void RankCoverDensity_with_normalization() - { - using var context = CreateContext(); - var rank = context.Customers - .Select( - c => EF.Functions.ToTsVector("a b c").RankCoverDensity( - EF.Functions.ToTsQuery("b"), - NpgsqlTsRankingNormalization.DivideByLength)) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank_cd(to_tsvector('a b c'), to_tsquery('b'), 2) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void RankCoverDensity_with_weights() - { - using var context = CreateContext(); - var rank = context.Customers - .Select( - c => EF.Functions.ToTsVector("a b c").RankCoverDensity( - new float[] { 1, 1, 1, 1 }, - EF.Functions.ToTsQuery("b"))) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank_cd(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b')) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void RankCoverDensity_with_weights_and_normalization() - { - using var context = CreateContext(); - var rank = context.Customers - .Select( - c => EF.Functions.ToTsVector("a b c").RankCoverDensity( - new float[] { 1, 1, 1, 1 }, - EF.Functions.ToTsQuery("b"), - NpgsqlTsRankingNormalization.DivideByLength)) - .First(); - - Assert.True(rank > 0); - AssertSql( - """ -SELECT ts_rank_cd(ARRAY[1,1,1,1]::real[], to_tsvector('a b c'), to_tsquery('b'), 2) -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Basic_where() - { - using var context = CreateContext(); - var count = context.Customers - .Count(c => EF.Functions.ToTsVector(c.ContactTitle).Matches(EF.Functions.ToTsQuery("owner"))); - - Assert.True(count > 0); - } - - [Fact] - public void Complex_query() - { - using var context = CreateContext(); - var headline = context.Customers - .Where( - c => EF.Functions.ToTsVector(c.ContactTitle) - .SetWeight(NpgsqlTsVector.Lexeme.Weight.A) - .Matches(EF.Functions.ToTsQuery("accounting").ToPhrase(EF.Functions.ToTsQuery("manager")))) - .Select( - c => EF.Functions.ToTsQuery("accounting").ToPhrase(EF.Functions.ToTsQuery("manager")) - .GetResultHeadline(c.ContactTitle)) - .First(); - - Assert.Equal("Accounting Manager", headline); - } - - [Fact] - public void Unaccent() - { - using var context = CreateContext(); - _ = context.Customers - .Select(x => EF.Functions.Unaccent(x.ContactName)) - .FirstOrDefault(); - - AssertSql( - """ -SELECT unaccent(c."ContactName") -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Unaccent_with_constant_regdictionary() - { - using var context = CreateContext(); - _ = context.Customers - .Select(x => EF.Functions.Unaccent("unaccent", x.ContactName)) - .FirstOrDefault(); - - AssertSql( - """ -SELECT unaccent('unaccent', c."ContactName") -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] - public void Unaccent_with_parameter_regdictionary() - { - using var context = CreateContext(); - var regDictionary = "unaccent"; - _ = context.Customers - .Select(x => EF.Functions.Unaccent(regDictionary, x.ContactName)) - .FirstOrDefault(); - - AssertSql( - """ -@regDictionary='unaccent' - -SELECT unaccent(@regDictionary::regdictionary, c."ContactName") -FROM "Customers" AS c -LIMIT 1 -"""); - } - - [Fact] // #1652 - public void Match_and_boolean_operator_precedence() - { - using var context = CreateContext(); - _ = context.Customers - .Count( - c => EF.Functions.ToTsVector(c.ContactTitle) - .Matches(EF.Functions.ToTsQuery("owner").Or(EF.Functions.ToTsQuery("foo")))); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE to_tsvector(c."ContactTitle") @@ (to_tsquery('owner') || to_tsquery('foo')) -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected NorthwindContext CreateContext() - => Fixture.CreateContext(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs deleted file mode 100644 index ecbf08ed77..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/FunkyDataQueryNpgsqlTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.FunkyDataModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class FunkyDataQueryNpgsqlTest : FunkyDataQueryTestBase -{ - // ReSharper disable once UnusedParameter.Local - public FunkyDataQueryNpgsqlTest(FunkyDataQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override Task String_FirstOrDefault_and_LastOrDefault(bool async) - => Task.CompletedTask; // Npgsql doesn't support reading an empty string as a char at the ADO level - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task String_starts_with_on_argument_with_escape_constant(bool async) - => await AssertQuery( - async, - ss => ss.Set().Where(c => c.FirstName.StartsWith("Some\\")), - ss => ss.Set().Where(c => c.FirstName != null && c.FirstName.StartsWith("Some\\"))); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task String_starts_with_on_argument_with_escape_parameter(bool async) - { - var param = "Some\\"; - await AssertQuery( - async, - ss => ss.Set().Where(c => c.FirstName.StartsWith(param)), - ss => ss.Set().Where(c => c.FirstName != null && c.FirstName.StartsWith(param))); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class FunkyDataQueryNpgsqlFixture : FunkyDataQueryFixtureBase, ITestSqlLoggerFactory - { - private FunkyDataData? _expectedData; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override FunkyDataContext CreateContext() - { - var context = base.CreateContext(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - return context; - } - - public override ISetSource GetExpectedData() - { - if (_expectedData is null) - { - _expectedData = (FunkyDataData)base.GetExpectedData(); - - var maxId = _expectedData.FunkyCustomers.Max(c => c.Id); - - var mutableCustomersOhYeah = (List)_expectedData.FunkyCustomers; - - mutableCustomersOhYeah.Add( - new FunkyCustomer - { - Id = maxId + 1, - FirstName = "Some\\Guy", - LastName = null - }); - } - - return _expectedData; - } - - protected override async Task SeedAsync(FunkyDataContext context) - { - context.FunkyCustomers.AddRange(GetExpectedData().Set()); - await context.SaveChangesAsync(); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs deleted file mode 100644 index 647336f0f9..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.EntityFrameworkCore.Query; - -/// -/// Provides unit tests for the fuzzystrmatch module function translations. -/// -/// -/// See: https://www.postgresql.org/docs/current/fuzzystrmatch.html -/// -public class FuzzyStringMatchQueryNpgsqlTest : IClassFixture -{ - private FuzzyStringMatchQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public FuzzyStringMatchQueryNpgsqlTest(FuzzyStringMatchQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region FunctionTests - - [Fact] - public void FuzzyStringMatchSoundex() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchSoundex(x.Text)) - .ToArray(); - - AssertContainsSql("""soundex(f."Text")"""); - } - - [Fact] - public void FuzzyStringMatchDifference() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchDifference(x.Text, "target")) - .ToArray(); - - AssertContainsSql("""difference(f."Text", 'target')"""); - } - - [Fact] - public void FuzzyStringMatchLevenshtein() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target")) - .ToArray(); - - AssertContainsSql("""levenshtein(f."Text", 'target')"""); - } - - [Fact] - public void FuzzyStringMatchLevenshtein_With_Costs() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target", 1, 2, 3)) - .ToArray(); - - AssertContainsSql("""levenshtein(f."Text", 'target', 1, 2, 3)"""); - } - - [Fact] - public void FuzzyStringMatchLevenshteinLessEqual() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 5)) - .ToArray(); - - AssertContainsSql("""levenshtein_less_equal(f."Text", 'target', 5)"""); - } - - [Fact] - public void FuzzyStringMatchLevenshteinLessEqual_With_Costs() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 1, 2, 3, 5)) - .ToArray(); - - AssertContainsSql("""levenshtein_less_equal(f."Text", 'target', 1, 2, 3, 5)"""); - } - - [Fact] - public void FuzzyStringMatchMetaphone() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchMetaphone(x.Text, 6)) - .ToArray(); - - AssertContainsSql("""metaphone(f."Text", 6)"""); - } - - [Fact] - public void FuzzyStringMatchDoubleMetaphone() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphone(x.Text)) - .ToArray(); - - AssertContainsSql("""dmetaphone(f."Text")"""); - } - - [Fact] - public void FuzzyStringMatchDoubleMetaphoneAlt() - { - using var context = CreateContext(); - var _ = context.FuzzyStringMatchTestEntities - .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphoneAlt(x.Text)) - .ToArray(); - - AssertContainsSql("""dmetaphone_alt(f."Text")"""); - } - - #endregion - - #region Fixtures - - /// - /// Represents a fixture suitable for testing fuzzy string match functions. - /// - public class FuzzyStringMatchQueryNpgsqlFixture : SharedStoreFixtureBase - { - protected override string StoreName - => "FuzzyStringMatchQueryTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override Task SeedAsync(FuzzyStringMatchContext context) - => FuzzyStringMatchContext.SeedAsync(context); - } - - /// - /// Represents an entity suitable for testing fuzzy string match functions. - /// - public class FuzzyStringMatchTestEntity - { - // ReSharper disable once UnusedMember.Global - /// - /// The primary key. - /// - [Key] - public int Id { get; set; } - - /// - /// Some text. - /// - public string Text { get; set; } = null!; - } - - /// - /// Represents a database suitable for testing fuzzy string match functions. - /// - public class FuzzyStringMatchContext : PoolableDbContext - { - /// - /// Represents a set of entities with properties. - /// - public DbSet FuzzyStringMatchTestEntities { get; set; } - - /// - /// Initializes a . - /// - /// - /// The options to be used for configuration. - /// - public FuzzyStringMatchContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.HasPostgresExtension("fuzzystrmatch"); - - base.OnModelCreating(modelBuilder); - } - - public static async Task SeedAsync(FuzzyStringMatchContext context) - { - for (var i = 1; i <= 9; i++) - { - var text = "Some text " + i; - context.FuzzyStringMatchTestEntities.Add( - new FuzzyStringMatchTestEntity { Id = i, Text = text }); - } - - await context.SaveChangesAsync(); - } - } - - #endregion - - #region Helpers - - protected FuzzyStringMatchContext CreateContext() - => Fixture.CreateContext(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - /// - /// Asserts that the SQL fragment appears in the logs. - /// - /// The SQL statement or fragment to search for in the logs. - private void AssertContainsSql(string sql) - => Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); - - #endregion -} diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs deleted file mode 100644 index cb5d7bc45d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarFromSqlQueryNpgsqlTest.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class GearsOfWarFromSqlQueryNpgsqlTest : GearsOfWarFromSqlQueryTestBase -{ - // ReSharper disable once UnusedParameter.Local - public GearsOfWarFromSqlQueryNpgsqlTest(GearsOfWarQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - 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" -""", - Sql); - } - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - private string Sql - => Fixture.TestSqlLoggerFactory.Sql; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs deleted file mode 100644 index acf4f1dbde..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlFixture.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; - -namespace Microsoft.EntityFrameworkCore.Query; - - -public class GearsOfWarQueryNpgsqlFixture : GearsOfWarQueryRelationalFixture -{ - protected override string StoreName - => "GearsOfWarQueryTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - private GearsOfWarData? _expectedData; - - static GearsOfWarQueryNpgsqlFixture() - { - // TODO: Switch to using NpgsqlDataSource -#pragma warning disable CS0618 // Type or member is obsolete - NpgsqlConnection.GlobalTypeMapper.EnableRecordsAsTuples(); -#pragma warning restore CS0618 // Type or member is obsolete - } - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("uuid-ossp"); - - modelBuilder.Entity().Property(c => c.IssueDate).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); - } - - public override ISetSource GetExpectedData() - { - if (_expectedData is null) - { - _expectedData = (GearsOfWarData)base.GetExpectedData(); - - // 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. - foreach (var mission in _expectedData.Missions) - { - mission.Timeline = new DateTimeOffset( - mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - mission.Duration = new TimeSpan( - mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); - } - } - - return _expectedData; - } - - 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. - // See https://github.com/dotnet/efcore/issues/26068 - - var squads = GearsOfWarData.CreateSquads(); - var missions = GearsOfWarData.CreateMissions(); - var squadMissions = GearsOfWarData.CreateSquadMissions(); - var cities = GearsOfWarData.CreateCities(); - var weapons = GearsOfWarData.CreateWeapons(); - var tags = GearsOfWarData.CreateTags(); - var gears = GearsOfWarData.CreateGears(); - var locustLeaders = GearsOfWarData.CreateLocustLeaders(); - var factions = GearsOfWarData.CreateFactions(); - var locustHighCommands = GearsOfWarData.CreateHighCommands(); - - foreach (var mission in missions) - { - mission.Timeline = new DateTimeOffset( - mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - mission.Duration = new TimeSpan( - mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); - } - - GearsOfWarData.WireUp( - squads, missions, squadMissions, cities, weapons, tags, gears, locustLeaders, factions, locustHighCommands); - - context.Squads.AddRange(squads); - context.Missions.AddRange(missions); - context.SquadMissions.AddRange(squadMissions); - context.Cities.AddRange(cities); - context.Weapons.AddRange(weapons); - context.Tags.AddRange(tags); - context.Gears.AddRange(gears); - context.LocustLeaders.AddRange(locustLeaders); - context.Factions.AddRange(factions); - context.LocustHighCommands.AddRange(locustHighCommands); - await context.SaveChangesAsync(); - - GearsOfWarData.WireUp2(locustLeaders, factions); - - await context.SaveChangesAsync(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs deleted file mode 100644 index 18bfe6e090..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs +++ /dev/null @@ -1,236 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -// ReSharper disable once UnusedMember.Global -public class GearsOfWarQueryNpgsqlTest : GearsOfWarQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public GearsOfWarQueryNpgsqlTest(GearsOfWarQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Byte array - - public override async Task Byte_array_filter_by_length_literal_does_not_cast_on_varbinary_n(bool async) - { - await base.Byte_array_filter_by_length_literal_does_not_cast_on_varbinary_n(async); - - AssertSql( - """ -SELECT s."Id", s."Banner", s."Banner5", s."InternalNumber", s."Name" -FROM "Squads" AS s -WHERE length(s."Banner5") = 5 -"""); - } - - #endregion Byte array - - #region DateTimeOffset - - // Not supported by design: we support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a - // non-UTC DateTimeOffset. - public override Task DateTimeOffsetNow_minus_timespan(bool async) - => Assert.ThrowsAsync(() => base.DateTimeOffsetNow_minus_timespan(async)); - - public override async Task DateTimeOffset_Date_returns_datetime(bool async) - { - var dateTimeOffset = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)); - - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Timeline.Date.ToLocalTime() >= dateTimeOffset.Date)); - - AssertSql( - """ -@dateTimeOffset_Date='0002-03-01T00:00:00.0000000' - -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 -"""); - } - - public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) - { - var dto = new DateTimeOffset(599898024001234567, new TimeSpan(0)); - var start = dto.AddDays(-1); - var end = dto.AddDays(1); - var dates = new[] { dto }; - - await AssertQuery( - async, - ss => ss.Set().Where( - m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline)), - assertEmpty: true); // TODO: Look into this - - AssertSql( - """ -@start='1902-01-01T10:00:00.1234567+00:00' (DbType = DateTime) -@end='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) -@dates={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) - -SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" -FROM "Missions" AS m -WHERE @start <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @end AND m."Timeline" = ANY (@dates) -"""); - } - - // Base implementation uses DateTimeOffset.Now, which we don't translate by design. Use DateTimeOffset.UtcNow instead. - public override async Task Select_datetimeoffset_comparison_in_projection(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set().Select(m => m.Timeline > DateTimeOffset.UtcNow)); - - AssertSql( - """ -SELECT m."Timeline" > now() -FROM "Missions" AS m -"""); - } - - #endregion DateTimeOffset - - #region DateTime - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Where_datetime_subtraction(bool async) - => AssertQuery( - async, - ss => ss.Set().Where( - m => - new DateTimeOffset(2, 3, 2, 8, 0, 0, new TimeSpan(-5, 0, 0)) - m.Timeline > TimeSpan.FromDays(3)), - assertEmpty: true); // TODO: Look into this - - #endregion DateTime - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Where_TimeSpan_TotalDays(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Duration.TotalDays < 0.042)); - - AssertSql( - """ -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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Where_TimeSpan_TotalHours(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Duration.TotalHours < 1.02)); - - AssertSql( - """ -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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Where_TimeSpan_TotalMinutes(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Duration.TotalMinutes < 61)); - - AssertSql( - """ -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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Where_TimeSpan_TotalSeconds(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Duration.TotalSeconds < 3700)); - - AssertSql( - """ -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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Where_TimeSpan_TotalMilliseconds(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Duration.TotalMilliseconds < 3700000)); - - AssertSql( - """ -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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_Property_Select_Sum_over_TimeSpan(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => EF.Functions.Sum(g.Select(o => o.Duration))), - ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => (TimeSpan?)new TimeSpan(g.Sum(o => o.Duration.Ticks)))); - - AssertSql( - """ -SELECT sum(m."Duration") -FROM "Missions" AS m -GROUP BY m."Id" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_Property_Select_Average_over_TimeSpan(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => EF.Functions.Average(g.Select(o => o.Duration))), - ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => (TimeSpan?)new TimeSpan((long)g.Average(o => o.Duration.Ticks)))); - - AssertSql( - """ -SELECT avg(m."Duration") -FROM "Missions" AS m -GROUP BY m."Id" -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/IncludeNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/IncludeNpgsqlFixture.cs deleted file mode 100644 index 4feaf08e0e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/IncludeNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class IncludeNpgsqlFixture : NorthwindQueryNpgsqlFixture -{ - protected override bool ShouldLogCategory(string logCategory) - => base.ShouldLogCategory(logCategory) || logCategory == DbLoggerCategory.Query.Name; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/IncludeOneToOneNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/IncludeOneToOneNpgsqlTest.cs deleted file mode 100644 index b39178b5fc..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/IncludeOneToOneNpgsqlTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -// ReSharper disable once UnusedMember.Global -public class IncludeOneToOneNpgsqlTest : IncludeOneToOneTestBase -{ - // ReSharper disable once UnusedParameter.Local - public IncludeOneToOneNpgsqlTest(OneToOneQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public class OneToOneQueryNpgsqlFixture : OneToOneQueryFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlFixture.cs deleted file mode 100644 index 26707e113a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class InheritanceRelationshipsQueryNpgsqlFixture : InheritanceRelationshipsQueryRelationalFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs deleted file mode 100644 index 819cc7338a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/InheritanceRelationshipsQueryNpgsqlTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class InheritanceRelationshipsQueryNpgsqlTest(InheritanceRelationshipsQueryNpgsqlFixture fixture) - : InheritanceRelationshipsQueryTestBase(fixture) -{ - protected override void ClearLog() { } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs deleted file mode 100644 index ce65575c4c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs +++ /dev/null @@ -1,3431 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class JsonQueryNpgsqlTest : JsonQueryRelationalTestBase -{ - public JsonQueryNpgsqlTest(JsonQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - 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_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_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); - - AssertSql( - ); - } - - public override async Task Project_json_collection_in_tracking_query_fails(bool async) - { - await base.Project_json_collection_in_tracking_query_fails(async); - - AssertSql( - ); - } - - public override async Task Project_json_entity_in_tracking_query_fails_even_when_owner_is_present(bool async) - { - await base.Project_json_entity_in_tracking_query_fails_even_when_owner_is_present(async); - - AssertSql( - ); - } - - 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_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_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_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_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_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); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Basic_json_projection_owned_collection_leaf(bool async) - { - await base.Basic_json_projection_owned_collection_leaf(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Basic_json_projection_scalar(bool async) - { - await base.Basic_json_projection_scalar(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" ->> 'Name' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_scalar_length(bool async) - { - await base.Json_scalar_length(async); - - AssertSql( - """ -SELECT j."Name" -FROM "JsonEntitiesBasic" AS j -WHERE length(j."OwnedReferenceRoot" ->> 'Name')::int > 2 -"""); - } - - public override async Task Basic_json_projection_enum_inside_json_entity(bool async) - { - await base.Basic_json_projection_enum_inside_json_entity(async); - - AssertSql( - """ -SELECT j."Id", CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Enum}' AS integer) AS "Enum" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_projection_enum_with_custom_conversion(bool async) - { - await base.Json_projection_enum_with_custom_conversion(async); - - AssertSql( - """ -SELECT j."Id", CAST(j.json_reference_custom_naming ->> '1CustomEnum' AS integer) AS "Enum" -FROM "JsonEntitiesCustomNaming" AS j -"""); - } - - public override async Task Json_property_in_predicate(bool async) - { - await base.Json_property_in_predicate(async); - - AssertSql( - """ -SELECT j."Id" -FROM "JsonEntitiesBasic" AS j -WHERE (CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Fraction}' AS numeric(18,2))) < 20.5 -"""); - } - - public override async Task Json_subquery_property_pushdown_length(bool async) - { - await base.Json_subquery_property_pushdown_length(async); - - AssertSql( - """ -@p='3' - -SELECT length(j1.c)::int -FROM ( - 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 - ) AS j0 -) AS j1 -"""); - } - - public override async Task Json_subquery_reference_pushdown_reference(bool async) - { - await base.Json_subquery_reference_pushdown_reference(async); - - AssertSql( - """ -@p='10' - -SELECT j1.c -> 'OwnedReferenceBranch', j1."Id" -FROM ( - 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 - ) AS j0 -) AS j1 -"""); - } - - public override async Task Json_subquery_reference_pushdown_reference_anonymous_projection(bool async) - { - await base.Json_subquery_reference_pushdown_reference_anonymous_projection(async); - - AssertSql( - """ -@__p_0='10' - -SELECT JSON_QUERY([t0].[c], '$.OwnedReferenceSharedBranch'), [t0].[Id], CAST(LEN([t0].[c0]) AS int) -FROM ( - SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[c0] - FROM ( - SELECT TOP(@__p_0) JSON_QUERY([j].[json_reference_shared], '$') AS [c], [j].[Id], CAST(JSON_VALUE([j].[json_reference_shared], '$.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf.SomethingSomething') AS nvarchar(max)) AS [c0] - FROM [JsonEntitiesBasic] AS [j] - ORDER BY [j].[Id] - ) AS [t] -) AS [t0] -"""); - } - - public override async Task Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(bool async) - { - await base.Json_subquery_reference_pushdown_reference_pushdown_anonymous_projection(async); - - AssertSql( - """ -@__p_0='10' - -SELECT JSON_QUERY([t2].[c],'$.OwnedReferenceSharedLeaf'), [t2].[Id], JSON_QUERY([t2].[c], '$.OwnedCollectionSharedLeaf'), [t2].[Length] -FROM ( - SELECT DISTINCT JSON_QUERY([t1].[c],'$') AS [c], [t1].[Id], [t1].[Length] - FROM ( - SELECT TOP(@__p_0) JSON_QUERY([t0].[c], '$.OwnedReferenceSharedBranch') AS [c], [t0].[Id], CAST(LEN([t0].[Scalar]) AS int) AS [Length] - FROM ( - SELECT DISTINCT JSON_QUERY([t].[c],'$') AS [c], [t].[Id], [t].[Scalar] - FROM ( - SELECT TOP(@__p_0) JSON_QUERY([j].[json_reference_shared], '$') AS [c], [j].[Id], CAST(JSON_VALUE([j].[json_reference_shared], '$.OwnedReferenceSharedBranch.OwnedReferenceSharedLeaf.SomethingSomething') AS nvarchar(max)) AS [Scalar] - FROM [JsonEntitiesBasic] AS [j] - ORDER BY [j].[Id] - ) AS [t] - ) AS [t0] - ORDER BY CAST(LEN([t0].[Scalar]) AS int) - ) AS [t1] -) AS [t2] -"""); - } - - public override async Task Json_subquery_reference_pushdown_reference_pushdown_reference(bool async) - { - await base.Json_subquery_reference_pushdown_reference_pushdown_reference(async); - - AssertSql( - """ -@p='10' - -SELECT j3.c -> 'OwnedReferenceLeaf', j3."Id" -FROM ( - SELECT DISTINCT j2.c AS c, j2."Id" - FROM ( - SELECT j1.c -> 'OwnedReferenceBranch' AS c, j1."Id" - FROM ( - 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 - ) AS j0 - ) AS j1 - ORDER BY j1.c0 ->> 'Name' NULLS FIRST - LIMIT @p - ) AS j2 -) AS j3 -"""); - } - - public override async Task Json_subquery_reference_pushdown_reference_pushdown_collection(bool async) - { - await base.Json_subquery_reference_pushdown_reference_pushdown_collection(async); - - AssertSql( - """ -@p='10' - -SELECT j3.c -> 'OwnedCollectionLeaf', j3."Id" -FROM ( - SELECT DISTINCT j2.c AS c, j2."Id" - FROM ( - SELECT j1.c -> 'OwnedReferenceBranch' AS c, j1."Id" - FROM ( - 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 - ) AS j0 - ) AS j1 - ORDER BY j1.c0 ->> 'Name' NULLS FIRST - LIMIT @p - ) AS j2 -) AS j3 -"""); - } - - public override async Task Json_subquery_reference_pushdown_property(bool async) - { - await base.Json_subquery_reference_pushdown_property(async); - - AssertSql( - """ -@p='10' - -SELECT j1.c ->> 'SomethingSomething' -FROM ( - 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 - ) AS j0 -) AS j1 -"""); - } - - public override async Task Custom_naming_projection_owner_entity(bool async) - { - await base.Custom_naming_projection_owner_entity(async); - - AssertSql( - """ -SELECT j."Id", j."Title", j.json_collection_custom_naming, j.json_reference_custom_naming -FROM "JsonEntitiesCustomNaming" AS j -"""); - } - - public override async Task Custom_naming_projection_owned_reference(bool async) - { - await base.Custom_naming_projection_owned_reference(async); - - AssertSql( - """ -SELECT j.json_reference_custom_naming -> 'Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸', j."Id" -FROM "JsonEntitiesCustomNaming" AS j -"""); - } - - public override async Task Custom_naming_projection_owned_collection(bool async) - { - await base.Custom_naming_projection_owned_collection(async); - - AssertSql( - """ -SELECT j.json_collection_custom_naming, j."Id" -FROM "JsonEntitiesCustomNaming" AS j -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Custom_naming_projection_owned_scalar(bool async) - { - await base.Custom_naming_projection_owned_scalar(async); - - AssertSql( - """ -SELECT CAST(j.json_reference_custom_naming #>> ARRAY['Custom#OwnedReferenceBranch`-=[]\;'',./~!@#$%^&*()_+{}|:"<>?独角兽π獨角獸','ユニコーンFraction一角獣']::text[] AS double precision) -FROM "JsonEntitiesCustomNaming" AS j -"""); - } - - public override async Task Custom_naming_projection_everything(bool async) - { - await base.Custom_naming_projection_everything(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 -> '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 -"""); - } - - public override async Task Project_entity_with_single_owned(bool async) - { - await base.Project_entity_with_single_owned(async); - - AssertSql( - """ -SELECT j."Id", j."Name", j."OwnedCollection" -FROM "JsonEntitiesSingleOwned" AS j -"""); - } - - public override async Task LeftJoin_json_entities(bool async) - { - await base.LeftJoin_json_entities(async); - - AssertSql( - """ -SELECT j."Id", j."Name", j."OwnedCollection", j0."Id", j0."EntityBasicId", j0."Name", j0."OwnedCollectionRoot", j0."OwnedReferenceRoot" -FROM "JsonEntitiesSingleOwned" AS j -LEFT JOIN "JsonEntitiesBasic" AS j0 ON j."Id" = j0."Id" -"""); - } - - public override async Task RightJoin_json_entities(bool async) - { - await base.RightJoin_json_entities(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."OwnedCollection" -FROM "JsonEntitiesBasic" AS j -RIGHT JOIN "JsonEntitiesSingleOwned" AS j0 ON j."Id" = j0."Id" -"""); - } - - public override async Task Left_join_json_entities_complex_projection(bool async) - { - await base.Left_join_json_entities_complex_projection(async); - - AssertSql( - """ -SELECT j."Id", j0."Id", j0."OwnedReferenceRoot", j0."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j0."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j0."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}' -FROM "JsonEntitiesSingleOwned" AS j -LEFT JOIN "JsonEntitiesBasic" AS j0 ON j."Id" = j0."Id" -"""); - } - - public override async Task Left_join_json_entities_json_being_inner(bool async) - { - await base.Left_join_json_entities_json_being_inner(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."OwnedCollection" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesSingleOwned" AS j0 ON j."Id" = j0."Id" -"""); - } - - public override async Task Left_join_json_entities_complex_projection_json_being_inner(bool async) - { - await base.Left_join_json_entities_complex_projection_json_being_inner(async); - - AssertSql( - """ -SELECT j."Id", j0."Id", j."OwnedReferenceRoot", j."OwnedReferenceRoot" -> 'OwnedReferenceBranch', j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j0."Name" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesSingleOwned" AS j0 ON j."Id" = j0."Id" -"""); - } - - public override async Task Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(bool async) - { - await base.Project_json_entity_FirstOrDefault_subquery_with_binding_on_top(async); - - AssertSql( - """ -SELECT ( - SELECT CAST(j0."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Date}' AS timestamp without time zone) - FROM "JsonEntitiesBasic" AS j0 - ORDER BY j0."Id" NULLS FIRST - LIMIT 1) -FROM "JsonEntitiesBasic" AS j -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(bool async) - { - await base.Project_json_entity_FirstOrDefault_subquery_with_entity_comparison_on_top(async); - - AssertSql( - @""); - } - - public override async Task Json_entity_with_inheritance_basic_projection(bool async) - { - await base.Json_entity_with_inheritance_basic_projection(async); - - AssertSql( - """ -SELECT j."Id", j."Discriminator", j."Name", j."Fraction", j."CollectionOnBase", j."ReferenceOnBase", j."CollectionOnDerived", j."ReferenceOnDerived" -FROM "JsonEntitiesInheritance" AS j -"""); - } - - public override async Task Json_entity_with_inheritance_project_derived(bool async) - { - await base.Json_entity_with_inheritance_project_derived(async); - - AssertSql( - """ -SELECT j."Id", j."Discriminator", j."Name", j."Fraction", j."CollectionOnBase", j."ReferenceOnBase", j."CollectionOnDerived", j."ReferenceOnDerived" -FROM "JsonEntitiesInheritance" AS j -WHERE j."Discriminator" = 'JsonEntityInheritanceDerived' -"""); - } - - public override async Task Json_entity_with_inheritance_project_navigations(bool async) - { - await base.Json_entity_with_inheritance_project_navigations(async); - - AssertSql( - """ -SELECT j."Id", j."ReferenceOnBase", j."CollectionOnBase" -FROM "JsonEntitiesInheritance" AS j -"""); - } - - public override async Task Json_entity_with_inheritance_project_navigations_on_derived(bool async) - { - await base.Json_entity_with_inheritance_project_navigations_on_derived(async); - - AssertSql( - """ -SELECT j."Id", j."ReferenceOnBase", j."ReferenceOnDerived", j."CollectionOnBase", j."CollectionOnDerived" -FROM "JsonEntitiesInheritance" AS j -WHERE j."Discriminator" = 'JsonEntityInheritanceDerived' -"""); - } - - public override async Task Json_entity_backtracking(bool async) - { - await base.Json_entity_backtracking(async); - - AssertSql( - @""); - } - - public override async Task Json_collection_index_in_projection_basic(bool async) - { - await base.Json_collection_index_in_projection_basic(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" -> 1, j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_ElementAt_in_projection(bool async) - { - await base.Json_collection_ElementAt_in_projection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" -> 1, j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_ElementAtOrDefault_in_projection(bool async) - { - await base.Json_collection_ElementAtOrDefault_in_projection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" -> 1, j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_project_collection(bool async) - { - await base.Json_collection_index_in_projection_project_collection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_ElementAt_project_collection(bool async) - { - await base.Json_collection_ElementAt_project_collection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_ElementAtOrDefault_project_collection(bool async) - { - await base.Json_collection_ElementAtOrDefault_project_collection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_parameter(bool async) - { - await base.Json_collection_index_in_projection_using_parameter(async); - - AssertSql( - """ -@prm='0' - -SELECT j."OwnedCollectionRoot" -> @prm, j."Id", @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_column(bool async) - { - await base.Json_collection_index_in_projection_using_column(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" -> j."Id", j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_untranslatable_client_method(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Json_collection_index_in_projection_using_untranslatable_client_method(async))).Message; - - Assert.Contains( - CoreStrings.QueryUnableToTranslateMethod( - "Microsoft.EntityFrameworkCore.Query.JsonQueryTestBase", - "MyMethod"), - message); - } - - public override async Task Json_collection_index_in_projection_using_untranslatable_client_method2(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Json_collection_index_in_projection_using_untranslatable_client_method2(async))).Message; - - Assert.Contains( - CoreStrings.QueryUnableToTranslateMethod( - "Microsoft.EntityFrameworkCore.Query.JsonQueryTestBase", - "MyMethod"), - message); - } - - public override async Task Json_collection_index_outside_bounds(bool async) - { - await base.Json_collection_index_outside_bounds(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" -> 25, j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_outside_bounds2(bool async) - { - await base.Json_collection_index_outside_bounds2(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,25}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_outside_bounds_with_property_access(bool async) - { - await base.Json_collection_index_outside_bounds_with_property_access(async); - - AssertSql( - """ -SELECT CAST(j."OwnedCollectionRoot" #>> '{25,Number}' AS integer) -FROM "JsonEntitiesBasic" AS j -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_index_in_projection_nested(bool async) - { - await base.Json_collection_index_in_projection_nested(async); - - AssertSql( - """ -@prm='1' - -SELECT j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm]::text[], j."Id", @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_nested_project_scalar(bool async) - { - await base.Json_collection_index_in_projection_nested_project_scalar(async); - - AssertSql( - """ -@prm='1' - -SELECT CAST(j."OwnedCollectionRoot" #>> ARRAY[0,'OwnedCollectionBranch',@prm,'Date']::text[] AS timestamp without time zone) -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_nested_project_reference(bool async) - { - await base.Json_collection_index_in_projection_nested_project_reference(async); - - AssertSql( - """ -@prm='1' - -SELECT j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm,'OwnedReferenceLeaf']::text[], j."Id", @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_nested_project_collection(bool async) - { - await base.Json_collection_index_in_projection_nested_project_collection(async); - - AssertSql( - """ -@prm='1' - -SELECT j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm,'OwnedCollectionLeaf']::text[], j."Id", @prm -FROM "JsonEntitiesBasic" AS j -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_index_in_projection_nested_project_collection_anonymous_projection(bool async) - { - await base.Json_collection_index_in_projection_nested_project_collection_anonymous_projection(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[0,'OwnedCollectionBranch',@prm,'OwnedCollectionLeaf']::text[], @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_predicate_using_constant(bool async) - { - await base.Json_collection_index_in_predicate_using_constant(async); - - AssertSql( - """ -SELECT j."Id" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> '{0,Name}') <> 'Foo' OR (j."OwnedCollectionRoot" #>> '{0,Name}') IS NULL -"""); - } - - public override async Task Json_collection_index_in_predicate_using_variable(bool async) - { - await base.Json_collection_index_in_predicate_using_variable(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> ARRAY[@prm,'Name']::text[]) <> 'Foo' OR (j."OwnedCollectionRoot" #>> ARRAY[@prm,'Name']::text[]) IS NULL -"""); - } - - public override async Task Json_collection_index_in_predicate_using_column(bool async) - { - await base.Json_collection_index_in_predicate_using_column(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> ARRAY[j."Id",'Name']::text[]) = 'e1_c2' -"""); - } - - public override async Task Json_collection_index_in_predicate_using_complex_expression1(bool async) - { - await base.Json_collection_index_in_predicate_using_complex_expression1(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> ARRAY[CASE - WHEN j."Id" = 1 THEN 0 - ELSE 1 -END,'Name']::text[]) = 'e1_c1' -"""); - } - - public override async Task Json_collection_index_in_predicate_using_complex_expression2(bool async) - { - await base.Json_collection_index_in_predicate_using_complex_expression2(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> ARRAY[( - SELECT max(j0."Id") - FROM "JsonEntitiesBasic" AS j0),'Name']::text[]) = 'e1_c2' -"""); - } - - public override async Task Json_collection_ElementAt_in_predicate(bool async) - { - await base.Json_collection_ElementAt_in_predicate(async); - - AssertSql( - """ -SELECT j."Id" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> '{1,Name}') <> 'Foo' OR (j."OwnedCollectionRoot" #>> '{1,Name}') IS NULL -"""); - } - - public override async Task Json_collection_index_in_predicate_nested_mix(bool async) - { - await base.Json_collection_index_in_predicate_nested_mix(async); - - AssertSql( - """ -@prm='0' - -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedCollectionRoot" #>> ARRAY[1,'OwnedCollectionBranch',@prm,'OwnedCollectionLeaf',j."Id" - 1,'SomethingSomething']::text[]) = 'e1_c2_c1_c1' -"""); - } - - public override async Task Json_collection_ElementAt_and_pushdown(bool async) - { - await base.Json_collection_ElementAt_and_pushdown(async); - - AssertSql( - """ -SELECT j."Id", CAST(j."OwnedCollectionRoot" #>> '{0,Number}' AS integer) AS "CollectionElement" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_Any_with_predicate(bool async) - { - await base.Json_collection_Any_with_predicate(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE EXISTS ( - SELECT 1 - FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ("OwnedReferenceLeaf" jsonb)) WITH ORDINALITY AS o - WHERE (o."OwnedReferenceLeaf" ->> 'SomethingSomething') = 'e1_r_c1_r') -"""); - } - - public override async Task Json_collection_Where_ElementAt(bool async) - { - await base.Json_collection_Where_ElementAt(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE ( - SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' - FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ( - "Date" timestamp without time zone, - "Enum" integer, - "Fraction" numeric(18,2), - "Id" integer, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o - WHERE o."Enum" = -3 - LIMIT 1 OFFSET 0) = 'e1_r_c2_r' -"""); - } - - public override async Task Json_collection_Skip(bool async) - { - await base.Json_collection_Skip(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE ( - SELECT o0.c - FROM ( - 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 o0 - LIMIT 1 OFFSET 0) = 'e1_r_c2_r' -"""); - } - - public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool async) - { - await base.Json_collection_OrderByDescending_Skip_ElementAt(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE ( - SELECT o0.c - FROM ( - 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, - "Fraction" numeric(18,2), - "Id" integer, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o - ORDER BY o."Date" DESC NULLS LAST - OFFSET 1 - ) AS o0 - ORDER BY o0.c0 DESC NULLS LAST - LIMIT 1 OFFSET 0) = 'e1_r_c1_r' -"""); - } - - public override async Task Json_collection_Distinct_Count_with_predicate(bool async) - { - await base.Json_collection_Distinct_Count_with_predicate(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE ( - SELECT count(*)::int - FROM ( - 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" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "NullableEnum" integer, - "NullableEnums" jsonb, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o - WHERE (o."OwnedReferenceLeaf" ->> 'SomethingSomething') = 'e1_r_c2_r' - ) AS o0) = 1 -"""); - } - - public override async Task Json_collection_within_collection_Count(bool async) - { - await base.Json_collection_within_collection_Count(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE EXISTS ( - SELECT 1 - 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 ( - "Date" timestamp without time zone, - "Enum" integer, - "Enums" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "NullableEnum" integer, - "NullableEnums" jsonb, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o0) = 2) -"""); - } - - public override async Task Json_collection_in_projection_with_composition_count(bool async) - { - await base.Json_collection_in_projection_with_composition_count(async); - - AssertSql( - """ -SELECT ( - SELECT count(*)::int - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ( - "Id" integer, - "Name" text, - "Names" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o) -FROM "JsonEntitiesBasic" AS j -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_in_projection_with_anonymous_projection_of_scalars(bool async) - { - await base.Json_collection_in_projection_with_anonymous_projection_of_scalars(async); - - AssertSql( - """ -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, - "Number" integer -)) WITH ORDINALITY AS o ON TRUE -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_scalars(bool async) - { - await base.Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_scalars(async); - - AssertSql( - """ -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, - "Number" integer - )) WITH ORDINALITY AS o - WHERE o."Name" = 'Foo' -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_primitive_arrays(bool async) - { - await base.Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_primitive_arrays(async); - - AssertSql( - """ -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" jsonb, - "Number" integer, - "Numbers" jsonb - )) WITH ORDINALITY AS o - WHERE o."Name" = 'Foo' -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_filter_in_projection(bool async) - { - await base.Json_collection_filter_in_projection(async); - - AssertSql( - """ -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."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" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o - WHERE o."Name" <> 'Foo' OR o."Name" IS NULL -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_nested_collection_filter_in_projection(bool async) - { - await base.Json_nested_collection_filter_in_projection(async); - - AssertSql( - """ -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, 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."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" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "NullableEnum" integer, - "NullableEnums" jsonb, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o0 - WHERE o0."Date" <> TIMESTAMP '2000-01-01T00:00:00' - ) AS o1 ON TRUE -) AS s ON TRUE -ORDER BY j."Id" NULLS FIRST, s.ordinality NULLS FIRST -"""); - } - - public override async Task Json_nested_collection_anonymous_projection_in_projection(bool async) - { - await base.Json_nested_collection_anonymous_projection_in_projection(async); - - AssertSql( - """ -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 ("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" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) 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_collection_skip_take_in_projection(bool async) - { - await base.Json_collection_skip_take_in_projection(async); - - AssertSql( - """ -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."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" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o - ORDER BY o."Name" NULLS FIRST - LIMIT 5 OFFSET 1 -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST, o0.c1 NULLS FIRST -"""); - } - - public override async Task Json_collection_skip_take_in_projection_project_into_anonymous_type(bool async) - { - await base.Json_collection_skip_take_in_projection_project_into_anonymous_type(async); - - AssertSql( - """ -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" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o - ORDER BY o."Name" NULLS FIRST - LIMIT 5 OFFSET 1 -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST, o0.c NULLS FIRST -"""); - } - - public override async Task Json_collection_skip_take_in_projection_with_json_reference_access_as_final_operation(bool async) - { - await base.Json_collection_skip_take_in_projection_with_json_reference_access_as_final_operation(async); - - AssertSql( - """ -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, - "Number" integer, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o - ORDER BY o."Name" NULLS FIRST - LIMIT 5 OFFSET 1 -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST, o0.c0 NULLS FIRST -"""); - } - - public override async Task Json_collection_distinct_in_projection(bool async) - { - await base.Json_collection_distinct_in_projection(async); - - AssertSql( - """ -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."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" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o -) 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 -"""); - } - - public override async Task Json_collection_anonymous_projection_distinct_in_projection(bool async) - { - await base.Json_collection_anonymous_projection_distinct_in_projection(async); - - AssertSql(""); - } - - public override async Task Json_collection_leaf_filter_in_projection(bool async) - { - await base.Json_collection_leaf_filter_in_projection(async); - - AssertSql( - """ -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 o0 ON TRUE -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_multiple_collection_projections(bool async) - { - await base.Json_multiple_collection_projections(async); - - AssertSql( - """ -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 o4 ON TRUE -LEFT JOIN LATERAL ( - 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" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o0 -) AS o1 ON TRUE -LEFT JOIN LATERAL ( - 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", 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" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "NullableEnum" integer, - "NullableEnums" jsonb, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) 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, 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 -"""); - } - - public override async Task Json_branch_collection_distinct_and_other_collection(bool async) - { - await base.Json_branch_collection_distinct_and_other_collection(async); - - AssertSql( - """ -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."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" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "NullableEnum" integer, - "NullableEnums" jsonb, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o -) AS o0 ON TRUE -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -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 -"""); - } - - public override async Task Json_leaf_collection_distinct_and_other_collection(bool async) - { - await base.Json_leaf_collection_distinct_and_other_collection(async); - - AssertSql( - """ -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 o0 ON TRUE -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST, o0."SomethingSomething" NULLS FIRST -"""); - } - - public override async Task Json_collection_of_primitives_SelectMany(bool async) - { - await base.Json_collection_of_primitives_SelectMany(async); - - AssertSql( - """ -SELECT n.element -FROM "JsonEntitiesBasic" AS j -JOIN LATERAL jsonb_array_elements_text(j."OwnedReferenceRoot" -> 'Names') WITH ORDINALITY AS n(element) ON TRUE -"""); - } - - public override async Task Json_collection_of_primitives_index_used_in_predicate(bool async) - { - await base.Json_collection_of_primitives_index_used_in_predicate(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedReferenceRoot" #>> '{Names,0}') = 'e1_r1' -"""); - } - - public override async Task Json_collection_of_primitives_index_used_in_projection(bool async) - { - await base.Json_collection_of_primitives_index_used_in_projection(async); - - AssertSql( - """ -SELECT CAST(j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,Enums,0}' AS integer) -FROM "JsonEntitiesBasic" AS j -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_of_primitives_index_used_in_orderby(bool async) - { - await base.Json_collection_of_primitives_index_used_in_orderby(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -ORDER BY CAST(j."OwnedReferenceRoot" #>> '{Numbers,0}' AS integer) NULLS FIRST -"""); - } - - public override async Task Json_collection_of_primitives_contains_in_predicate(bool async) - { - await base.Json_collection_of_primitives_contains_in_predicate(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -WHERE (j."OwnedReferenceRoot" -> 'Names') @> to_jsonb('e1_r1'::text) -"""); - } - - public override async Task Json_collection_index_with_parameter_Select_ElementAt(bool async) - { - await base.Json_collection_index_with_parameter_Select_ElementAt(async); - - AssertSql( - """ -@prm='0' - -SELECT j."Id", ( - SELECT 'Foo' - FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch']::text[]) AS ( - "Date" timestamp without time zone, - "Enum" integer, - "Enums" jsonb, - "Fraction" numeric(18,2), - "Id" integer, - "NullableEnum" integer, - "NullableEnums" jsonb, - "OwnedCollectionLeaf" jsonb, - "OwnedReferenceLeaf" jsonb - )) WITH ORDINALITY AS o - LIMIT 1 OFFSET 0) AS "CollectionElement" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_with_expression_Select_ElementAt(bool async) - { - await base.Json_collection_index_with_expression_Select_ElementAt(async); - - AssertSql( - """ -@prm='0' - -SELECT j."OwnedCollectionRoot" #>> ARRAY[@prm + j."Id",'OwnedCollectionBranch',0,'OwnedReferenceLeaf','SomethingSomething']::text[] -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_Select_entity_collection_ElementAt(bool async) - { - await base.Json_collection_Select_entity_collection_ElementAt(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" #> '{0,OwnedCollectionBranch}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_Select_entity_ElementAt(bool async) - { - await base.Json_collection_Select_entity_ElementAt(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j."Id" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_Select_entity_in_anonymous_object_ElementAt(bool async) - { - await base.Json_collection_Select_entity_in_anonymous_object_ElementAt(async); - - AssertSql( - """ -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 ("OwnedReferenceBranch" jsonb)) WITH ORDINALITY AS o - LIMIT 1 OFFSET 0 -) AS o0 ON TRUE -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async) - { - await base.Json_collection_Select_entity_with_initializer_ElementAt(async); - - AssertSql( - """ -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" jsonb, - "Number" integer, - "Numbers" jsonb, - "OwnedCollectionBranch" jsonb, - "OwnedReferenceBranch" jsonb - )) WITH ORDINALITY AS o - LIMIT 1 OFFSET 0 -) AS o0 ON TRUE -"""); - } - - public override async Task Json_projection_deduplication_with_collection_indexer_in_original(bool async) - { - await base.Json_projection_deduplication_with_collection_indexer_in_original(async); - - AssertSql( - """ -SELECT j."Id", j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j."OwnedCollectionRoot" -> 0, j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch,OwnedCollectionLeaf}' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_projection_deduplication_with_collection_indexer_in_target(bool async) - { - await base.Json_projection_deduplication_with_collection_indexer_in_target(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,1}', j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> ARRAY['OwnedReferenceBranch','OwnedCollectionLeaf',@prm]::text[], @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(bool async) - { - await base.Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(async); - - AssertSql( - """ -@prm='1' - -SELECT j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',0,'OwnedCollectionLeaf',@prm]::text[], j."Id", @prm, j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@prm]::text[], j."OwnedReferenceRoot" -> 'OwnedCollectionBranch', j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0}' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async) - { - await base.Json_collection_index_in_projection_using_constant_when_owner_is_present(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" -> 1 -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_constant_when_owner_is_not_present(bool async) - { - await base.Json_collection_index_in_projection_using_constant_when_owner_is_not_present(async); - - AssertSql( - """ -SELECT j."Id", j."OwnedCollectionRoot" -> 1 -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_parameter_when_owner_is_present(bool async) - { - await base.Json_collection_index_in_projection_using_parameter_when_owner_is_present(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" -> @prm, @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_using_parameter_when_owner_is_not_present(bool async) - { - await base.Json_collection_index_in_projection_using_parameter_when_owner_is_not_present(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."OwnedCollectionRoot" -> @prm, @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(bool async) - { - await base.Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_not_present(bool async) - { - await base.Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_not_present(async); - - AssertSql( - """ -SELECT j."Id", j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch}' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(bool async) - { - await base.Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch']::text[], @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_not_present(bool async) - { - await base.Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_not_present(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch']::text[], @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_when_owner_is_present_misc1(bool async) - { - await base.Json_collection_index_in_projection_when_owner_is_present_misc1(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> ARRAY[1,'OwnedCollectionBranch',@prm]::text[], @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_when_owner_is_not_present_misc1(bool async) - { - await base.Json_collection_index_in_projection_when_owner_is_not_present_misc1(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[1,'OwnedCollectionBranch',@prm]::text[], @prm -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_when_owner_is_present_misc2(bool async) - { - await base.Json_collection_index_in_projection_when_owner_is_present_misc2(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,1}' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_when_owner_is_not_present_misc2(bool async) - { - await base.Json_collection_index_in_projection_when_owner_is_not_present_misc2(async); - - AssertSql( - """ -SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,1}' -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_when_owner_is_present_multiple(bool async) - { - await base.Json_collection_index_in_projection_when_owner_is_present_multiple(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',1]::text[], @prm, j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch,1,OwnedReferenceLeaf}', j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',j."Id"]::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',1,'OwnedReferenceLeaf']::text[], j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',j."Id"]::text[] -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_collection_index_in_projection_when_owner_is_not_present_multiple(bool async) - { - await base.Json_collection_index_in_projection_when_owner_is_not_present_multiple(async); - - AssertSql( - """ -@prm='1' - -SELECT j."Id", j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',1]::text[], @prm, j."OwnedCollectionRoot" #> '{1,OwnedCollectionBranch,1,OwnedReferenceLeaf}', j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[@prm,'OwnedCollectionBranch',j."Id"]::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',1,'OwnedReferenceLeaf']::text[], j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedReferenceBranch']::text[], j."OwnedCollectionRoot" #> ARRAY[j."Id",'OwnedCollectionBranch',j."Id"]::text[] -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_scalar_required_null_semantics(bool async) - { - await base.Json_scalar_required_null_semantics(async); - - AssertSql( - """ -SELECT j."Name" -FROM "JsonEntitiesBasic" AS j -WHERE (CAST(j."OwnedReferenceRoot" ->> 'Number' AS integer)) <> length(j."OwnedReferenceRoot" ->> 'Name')::int OR (j."OwnedReferenceRoot" ->> 'Name') IS NULL -"""); - } - - public override async Task Json_scalar_optional_null_semantics(bool async) - { - await base.Json_scalar_optional_null_semantics(async); - - AssertSql( - """ -SELECT j."Name" -FROM "JsonEntitiesBasic" AS j -WHERE ((j."OwnedReferenceRoot" ->> 'Name') <> (j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}') OR (j."OwnedReferenceRoot" ->> 'Name') IS NULL OR (j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}') IS NULL) AND ((j."OwnedReferenceRoot" ->> 'Name') IS NOT NULL OR (j."OwnedReferenceRoot" #>> '{OwnedReferenceBranch,OwnedReferenceLeaf,SomethingSomething}') IS NOT NULL) -"""); - } - - public override async Task Group_by_on_json_scalar(bool async) - { - await base.Group_by_on_json_scalar(async); - - AssertSql( - """ -SELECT j0."Key", count(*)::int AS "Count" -FROM ( - SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j -) AS j0 -GROUP BY j0."Key" -"""); - } - - public override async Task Group_by_on_json_scalar_using_collection_indexer(bool async) - { - await base.Group_by_on_json_scalar_using_collection_indexer(async); - - AssertSql( - """ -SELECT j0."Key", count(*)::int AS "Count" -FROM ( - SELECT j."OwnedCollectionRoot" #>> '{0,Name}' AS "Key" - FROM "JsonEntitiesBasic" AS j -) AS j0 -GROUP BY j0."Key" -"""); - } - - public override async Task Group_by_First_on_json_scalar(bool async) - { - await base.Group_by_First_on_json_scalar(async); - - AssertSql( - """ -SELECT j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 -FROM ( - SELECT j0."Key" - FROM ( - SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j - ) AS j0 - GROUP BY j0."Key" -) AS j3 -LEFT JOIN ( - SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c AS c, j4.c0 AS c0, j4."Key" - FROM ( - 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 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" -"""); - } - - public override async Task Group_by_FirstOrDefault_on_json_scalar(bool async) - { - await base.Group_by_FirstOrDefault_on_json_scalar(async); - - AssertSql( - """ -SELECT j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 -FROM ( - SELECT j0."Key" - FROM ( - SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j - ) AS j0 - GROUP BY j0."Key" -) AS j3 -LEFT JOIN ( - SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c AS c, j4.c0 AS c0, j4."Key" - FROM ( - 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 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" -"""); - } - - public override async Task Group_by_Skip_Take_on_json_scalar(bool async) - { - await base.Group_by_Skip_Take_on_json_scalar(async); - - AssertSql( - """ -SELECT j3."Key", j5."Id", j5."EntityBasicId", j5."Name", j5.c, j5.c0 -FROM ( - SELECT j0."Key" - FROM ( - SELECT j."OwnedReferenceRoot" ->> 'Name' AS "Key" - FROM "JsonEntitiesBasic" AS j - ) AS j0 - GROUP BY j0."Key" -) AS j3 -LEFT JOIN ( - SELECT j4."Id", j4."EntityBasicId", j4."Name", j4.c, j4.c0, j4."Key" - FROM ( - 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 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 -"""); - } - - public override async Task Group_by_json_scalar_Orderby_json_scalar_FirstOrDefault(bool async) - { - await base.Group_by_json_scalar_Orderby_json_scalar_FirstOrDefault(async); - - AssertSql( - @""); - } - - public override async Task Group_by_json_scalar_Skip_First_project_json_scalar(bool async) - { - await base.Group_by_json_scalar_Skip_First_project_json_scalar(async); - - AssertSql( - """ -SELECT ( - SELECT CAST(j1.c0 #>> '{OwnedReferenceBranch,Enum}' AS integer) - FROM ( - 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 j0 -GROUP BY j0."Key" -"""); - } - - public override async Task Json_with_include_on_json_entity(bool async) - { - await base.Json_with_include_on_json_entity(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Json_with_include_on_entity_reference(bool async) - { - await base.Json_with_include_on_entity_reference(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" -"""); - } - - public override async Task Json_with_include_on_entity_collection(bool async) - { - await base.Json_with_include_on_entity_collection(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Entity_including_collection_with_json(bool async) - { - await base.Entity_including_collection_with_json(async); - - AssertSql( - """ -SELECT e."Id", e."Name", j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "EntitiesBasic" AS e -LEFT JOIN "JsonEntitiesBasic" AS j ON e."Id" = j."EntityBasicId" -ORDER BY e."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_include_on_entity_collection_and_reference(bool async) - { - await base.Json_with_include_on_entity_collection_and_reference(async); - - AssertSql( - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot", j0."Id", j0."Name", j0."ParentId", j1."Id", j1."Name", j1."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" -LEFT JOIN "JsonEntitiesBasicForCollection" AS j1 ON j."Id" = j1."ParentId" -ORDER BY j."Id" NULLS FIRST, j0."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_json_reference_leaf_and_entity_collection(bool async) - { - await base.Json_with_projection_of_json_reference_leaf_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j."Id", j0."Id", j0."Name", j0."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_json_reference_and_entity_collection(bool async) - { - await base.Json_with_projection_of_json_reference_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot", j."Id", j0."Id", j0."Name", j0."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_multiple_json_references_and_entity_collection(bool async) - { - await base.Json_with_projection_of_multiple_json_references_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot", j."Id", j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j0."Id", j0."Name", j0."ParentId", j."OwnedCollectionRoot" #> '{1,OwnedReferenceBranch,OwnedReferenceLeaf}', j."OwnedCollectionRoot" #> '{0,OwnedCollectionBranch,0,OwnedReferenceLeaf}' -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_json_collection_leaf_and_entity_collection(bool async) - { - await base.Json_with_projection_of_json_collection_leaf_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."Id", j0."Id", j0."Name", j0."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_json_collection_and_entity_collection(bool async) - { - await base.Json_with_projection_of_json_collection_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot", j."Id", j0."Id", j0."Name", j0."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" -ORDER BY j."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_json_collection_element_and_entity_collection(bool async) - { - await base.Json_with_projection_of_json_collection_element_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedCollectionRoot" -> 0, j."Id", j0."Id", j0."Name", j0."ParentId", j1."Id", j1."Name", j1."ParentId" -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" -LEFT JOIN "JsonEntitiesBasicForCollection" AS j1 ON j."Id" = j1."ParentId" -ORDER BY j."Id" NULLS FIRST, j0."Id" NULLS FIRST -"""); - } - - public override async Task Json_with_projection_of_mix_of_json_collections_json_references_and_entity_collection(bool async) - { - await base.Json_with_projection_of_mix_of_json_collections_json_references_and_entity_collection(async); - - AssertSql( - """ -SELECT j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf}', j."Id", j0."Id", j0."Name", j0."ParentId", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedReferenceLeaf}', j1."Id", j1."Name", j1."ParentId", j."OwnedReferenceRoot" #> '{OwnedReferenceBranch,OwnedCollectionLeaf,0}', j."OwnedReferenceRoot" -> 'OwnedCollectionBranch', j."OwnedCollectionRoot", j."OwnedCollectionRoot" #> '{0,OwnedReferenceBranch}', j."OwnedCollectionRoot" #> '{0,OwnedCollectionBranch}' -FROM "JsonEntitiesBasic" AS j -LEFT JOIN "JsonEntitiesBasicForReference" AS j0 ON j."Id" = j0."ParentId" -LEFT JOIN "JsonEntitiesBasicForCollection" AS j1 ON j."Id" = j1."ParentId" -ORDER BY j."Id" NULLS FIRST, j0."Id" NULLS FIRST -"""); - -// AssertSql( -//""" -//SELECT JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedCollectionLeaf'), [j].[Id], [j0].[Id], [j0].[Name], [j0].[ParentId], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedReferenceLeaf'), [j1].[Id], [j1].[Name], [j1].[ParentId], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch'), [j].[OwnedCollectionRoot] -//FROM [JsonEntitiesBasic] AS [j] -//LEFT JOIN [JsonEntitiesBasicForReference] AS [j0] ON [j].[Id] = [j0].[ParentId] -//LEFT JOIN [JsonEntitiesBasicForCollection] AS [j1] ON [j].[Id] = [j1].[ParentId] -//ORDER BY [j].[Id], [j0].[Id] -//"""); - } - - public override async Task Json_all_types_entity_projection(bool async) - { - await base.Json_all_types_entity_projection(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 -"""); - } - - public override async Task Json_all_types_projection_from_owned_entity_reference(bool async) - { - await base.Json_all_types_projection_from_owned_entity_reference(async); - - AssertSql( - """ -SELECT j."Reference", j."Id" -FROM "JsonEntitiesAllTypes" AS j -"""); - } - - public override async Task Json_all_types_projection_individual_properties(bool async) - { - await base.Json_all_types_projection_individual_properties(async); - - AssertSql( - """ -SELECT j."Reference" ->> 'TestDefaultString' AS "TestDefaultString", j."Reference" ->> 'TestMaxLengthString' AS "TestMaxLengthString", CAST(j."Reference" ->> 'TestBoolean' AS boolean) AS "TestBoolean", CAST(j."Reference" ->> 'TestByte' AS smallint) AS "TestByte", CAST(j."Reference" ->> 'TestCharacter' AS character(1)) AS "TestCharacter", CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone) AS "TestDateTime", CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone) AS "TestDateTimeOffset", CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3)) AS "TestDecimal", CAST(j."Reference" ->> 'TestDouble' AS double precision) AS "TestDouble", CAST(j."Reference" ->> 'TestGuid' AS uuid) AS "TestGuid", CAST(j."Reference" ->> 'TestInt16' AS smallint) AS "TestInt16", CAST(j."Reference" ->> 'TestInt32' AS integer) AS "TestInt32", CAST(j."Reference" ->> 'TestInt64' AS bigint) AS "TestInt64", CAST(j."Reference" ->> 'TestSignedByte' AS smallint) AS "TestSignedByte", CAST(j."Reference" ->> 'TestSingle' AS real) AS "TestSingle", CAST(j."Reference" ->> 'TestTimeSpan' AS interval) AS "TestTimeSpan", CAST(j."Reference" ->> 'TestDateOnly' AS date) AS "TestDateOnly", CAST(j."Reference" ->> 'TestTimeOnly' AS time without time zone) AS "TestTimeOnly", CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer) AS "TestUnsignedInt16", CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint) AS "TestUnsignedInt32", CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0)) AS "TestUnsignedInt64", CAST(j."Reference" ->> 'TestEnum' AS integer) AS "TestEnum", CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer) AS "TestEnumWithIntConverter", CAST(j."Reference" ->> 'TestNullableEnum' AS integer) AS "TestNullableEnum", CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer) AS "TestNullableEnumWithIntConverter", j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls' AS "TestNullableEnumWithConverterThatHandlesNulls" -FROM "JsonEntitiesAllTypes" AS j -"""); - } - - public override async Task Json_boolean_predicate(bool async) - { - await base.Json_boolean_predicate(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 CAST(j."Reference" ->> 'TestBoolean' AS boolean) -"""); - } - - public override async Task Json_boolean_predicate_negated(bool async) - { - await base.Json_boolean_predicate_negated(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 NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) -"""); - } - - public override async Task Json_boolean_projection(bool async) - { - await base.Json_boolean_projection(async); - - AssertSql( - """ -SELECT CAST(j."Reference" ->> 'TestBoolean' AS boolean) -FROM "JsonEntitiesAllTypes" AS j -"""); - } - - public override async Task Json_boolean_projection_negated(bool async) - { - await base.Json_boolean_projection_negated(async); - - AssertSql( - """ -SELECT NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) -FROM "JsonEntitiesAllTypes" AS j -"""); - } - - public override async Task Json_predicate_on_default_string(bool async) - { - await base.Json_predicate_on_default_string(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 (j."Reference" ->> 'TestDefaultString') <> 'MyDefaultStringInReference1' OR (j."Reference" ->> 'TestDefaultString') IS NULL -"""); - } - - public override async Task Json_predicate_on_max_length_string(bool async) - { - await base.Json_predicate_on_max_length_string(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 (j."Reference" ->> 'TestMaxLengthString') <> 'Foo' OR (j."Reference" ->> 'TestMaxLengthString') IS NULL -"""); - } - - public override async Task Json_predicate_on_string_condition(bool async) - { - await base.Json_predicate_on_string_condition(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 CASE - WHEN NOT (CAST(j."Reference" ->> 'TestBoolean' AS boolean)) THEN j."Reference" ->> 'TestMaxLengthString' - ELSE j."Reference" ->> 'TestDefaultString' -END = 'MyDefaultStringInReference1' -"""); - } - - public override async Task Json_predicate_on_byte(bool async) - { - await base.Json_predicate_on_byte(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 (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."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 -"""); - } - - public override async Task Json_predicate_on_datetime(bool async) - { - await base.Json_predicate_on_datetime(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 (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 -"""); - } - - public override async Task Json_predicate_on_datetimeoffset(bool async) - { - await base.Json_predicate_on_datetimeoffset(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 (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 -"""); - } - - public override async Task Json_predicate_on_decimal(bool async) - { - await base.Json_predicate_on_decimal(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 (CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3))) <> 1.35 OR (CAST(j."Reference" ->> 'TestDecimal' AS numeric(18,3))) IS NULL -"""); - } - - public override async Task Json_predicate_on_double(bool async) - { - await base.Json_predicate_on_double(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 (CAST(j."Reference" ->> 'TestDouble' AS double precision)) <> 33.25 OR (CAST(j."Reference" ->> 'TestDouble' AS double precision)) IS NULL -"""); - } - - public override async Task Json_predicate_on_enum(bool async) - { - await base.Json_predicate_on_enum(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 (CAST(j."Reference" ->> 'TestEnum' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestEnum' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_enumwithintconverter(bool async) - { - await base.Json_predicate_on_enumwithintconverter(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 (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) <> -3 OR (CAST(j."Reference" ->> 'TestEnumWithIntConverter' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_guid(bool async) - { - await base.Json_predicate_on_guid(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 (CAST(j."Reference" ->> 'TestGuid' AS uuid)) <> '00000000-0000-0000-0000-000000000000' OR (CAST(j."Reference" ->> 'TestGuid' AS uuid)) IS NULL -"""); - } - - public override async Task Json_predicate_on_int16(bool async) - { - await base.Json_predicate_on_int16(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 (CAST(j."Reference" ->> 'TestInt16' AS smallint)) <> 3 OR (CAST(j."Reference" ->> 'TestInt16' AS smallint)) IS NULL -"""); - } - - public override async Task Json_predicate_on_int32(bool async) - { - await base.Json_predicate_on_int32(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 (CAST(j."Reference" ->> 'TestInt32' AS integer)) <> 33 OR (CAST(j."Reference" ->> 'TestInt32' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_int64(bool async) - { - await base.Json_predicate_on_int64(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 (CAST(j."Reference" ->> 'TestInt64' AS bigint)) <> 333 OR (CAST(j."Reference" ->> 'TestInt64' AS bigint)) IS NULL -"""); - } - - public override async Task Json_predicate_on_nullableenum1(bool async) - { - await base.Json_predicate_on_nullableenum1(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 (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) <> -1 OR (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_nullableenum2(bool async) - { - await base.Json_predicate_on_nullableenum2(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 (CAST(j."Reference" ->> 'TestNullableEnum' AS integer)) IS NOT NULL -"""); - } - - public override async Task Json_predicate_on_nullableenumwithconverter1(bool async) - { - await base.Json_predicate_on_nullableenumwithconverter1(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 (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) <> 2 OR (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_nullableenumwithconverter2(bool async) - { - await base.Json_predicate_on_nullableenumwithconverter2(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 (CAST(j."Reference" ->> 'TestNullableEnumWithIntConverter' AS integer)) IS NOT NULL -"""); - } - - public override async Task Json_predicate_on_nullableenumwithconverterthathandlesnulls1(bool async) - { - await base.Json_predicate_on_nullableenumwithconverterthathandlesnulls1(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 (j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls') <> 'One' OR (j."Reference" ->> 'TestNullableEnumWithConverterThatHandlesNulls') IS NULL -"""); - } - - public override async Task Json_predicate_on_nullableenumwithconverterthathandlesnulls2(bool async) - { - await base.Json_predicate_on_nullableenumwithconverterthathandlesnulls2(async); - - AssertSql( - """ -x -"""); - } - - public override async Task Json_predicate_on_nullableint321(bool async) - { - await base.Json_predicate_on_nullableint321(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 (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) <> 100 OR (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_nullableint322(bool async) - { - await base.Json_predicate_on_nullableint322(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 (CAST(j."Reference" ->> 'TestNullableInt32' AS integer)) IS NOT NULL -"""); - } - - public override async Task Json_predicate_on_signedbyte(bool async) - { - await base.Json_predicate_on_signedbyte(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 (CAST(j."Reference" ->> 'TestSignedByte' AS smallint)) <> 100 OR (CAST(j."Reference" ->> 'TestSignedByte' AS smallint)) IS NULL -"""); - } - - public override async Task Json_predicate_on_single(bool async) - { - await base.Json_predicate_on_single(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 (CAST(j."Reference" ->> 'TestSingle' AS real)) <> 10.4 OR (CAST(j."Reference" ->> 'TestSingle' AS real)) IS NULL -"""); - } - - public override async Task Json_predicate_on_timespan(bool async) - { - await base.Json_predicate_on_timespan(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 (CAST(j."Reference" ->> 'TestTimeSpan' AS interval)) <> INTERVAL '03:02:00' OR (CAST(j."Reference" ->> 'TestTimeSpan' AS interval)) IS NULL -"""); - } - - public override async Task Json_predicate_on_dateonly(bool async) - { - await base.Json_predicate_on_dateonly(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 (CAST(j."Reference" ->> 'TestDateOnly' AS date)) <> DATE '0003-02-01' OR (CAST(j."Reference" ->> 'TestDateOnly' AS date)) IS NULL -"""); - } - - public override async Task Json_predicate_on_timeonly(bool async) - { - await base.Json_predicate_on_timeonly(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 (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 -"""); - } - - public override async Task Json_predicate_on_unisgnedint16(bool async) - { - await base.Json_predicate_on_unisgnedint16(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 (CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer)) <> 100 OR (CAST(j."Reference" ->> 'TestUnsignedInt16' AS integer)) IS NULL -"""); - } - - public override async Task Json_predicate_on_unsignedint32(bool async) - { - await base.Json_predicate_on_unsignedint32(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 (CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint)) <> 1000 OR (CAST(j."Reference" ->> 'TestUnsignedInt32' AS bigint)) IS NULL -"""); - } - - public override async Task Json_predicate_on_unsignedint64(bool async) - { - await base.Json_predicate_on_unsignedint64(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 (CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0))) <> 10000.0 OR (CAST(j."Reference" ->> 'TestUnsignedInt64' AS numeric(20,0))) IS NULL -"""); - } - - public override async Task Json_predicate_on_bool_converted_to_int_zero_one(bool async) - { - await base.Json_predicate_on_bool_converted_to_int_zero_one(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (CAST(j."Reference" ->> 'BoolConvertedToIntZeroOne' AS integer)) = 1 -"""); - } - - public override async Task Json_predicate_on_bool_converted_to_int_zero_one_with_explicit_comparison(bool async) - { - await base.Json_predicate_on_bool_converted_to_int_zero_one_with_explicit_comparison(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (CAST(j."Reference" ->> 'BoolConvertedToIntZeroOne' AS integer)) = 0 -"""); - } - - public override async Task Json_predicate_on_bool_converted_to_string_True_False(bool async) - { - await base.Json_predicate_on_bool_converted_to_string_True_False(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (j."Reference" ->> 'BoolConvertedToStringTrueFalse') = 'True' -"""); - } - - public override async Task Json_predicate_on_bool_converted_to_string_True_False_with_explicit_comparison(bool async) - { - await base.Json_predicate_on_bool_converted_to_string_True_False_with_explicit_comparison(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (j."Reference" ->> 'BoolConvertedToStringTrueFalse') = 'True' -"""); - } - - public override async Task Json_predicate_on_bool_converted_to_string_Y_N(bool async) - { - await base.Json_predicate_on_bool_converted_to_string_Y_N(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (j."Reference" ->> 'BoolConvertedToStringYN') = 'Y' -"""); - } - - public override async Task Json_predicate_on_bool_converted_to_string_Y_N_with_explicit_comparison(bool async) - { - await base.Json_predicate_on_bool_converted_to_string_Y_N_with_explicit_comparison(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (j."Reference" ->> 'BoolConvertedToStringYN') = 'N' -"""); - } - - public override async Task Json_predicate_on_int_zero_one_converted_to_bool(bool async) - { - await base.Json_predicate_on_int_zero_one_converted_to_bool(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (CAST(j."Reference" ->> 'IntZeroOneConvertedToBool' AS boolean)) = TRUE -"""); - } - - public override async Task Json_predicate_on_string_True_False_converted_to_bool(bool async) - { - await base.Json_predicate_on_string_True_False_converted_to_bool(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (CAST(j."Reference" ->> 'StringTrueFalseConvertedToBool' AS boolean)) = FALSE -"""); - } - - public override async Task Json_predicate_on_string_Y_N_converted_to_bool(bool async) - { - await base.Json_predicate_on_string_Y_N_converted_to_bool(async); - - AssertSql( - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE (CAST(j."Reference" ->> 'StringYNConvertedToBool' AS boolean)) = FALSE -"""); - } - - public override async Task FromSql_on_entity_with_json_basic(bool async) - { - await base.FromSql_on_entity_with_json_basic(async); - - AssertSql( - """ -SELECT m."Id", m."EntityBasicId", m."Name", m."OwnedCollectionRoot", m."OwnedReferenceRoot" -FROM ( - SELECT * FROM "JsonEntitiesBasic" AS j -) AS m -"""); - } - - 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); - - AssertSql( - """ -SELECT m."OwnedReferenceRoot" -> 'OwnedReferenceBranch', m."Id" -FROM ( - SELECT * FROM "JsonEntitiesBasic" AS j -) AS m -"""); - } - - 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); - - AssertSql( - """ -SELECT m."OwnedReferenceRoot" -> 'OwnedCollectionBranch', m."Id" -FROM ( - SELECT * FROM "JsonEntitiesBasic" AS j -) AS m -"""); - } - - 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); - - AssertSql( - """ -SELECT m."Id", m."Discriminator", m."Name", m."Fraction", m."CollectionOnBase", m."ReferenceOnBase", m."CollectionOnDerived", m."ReferenceOnDerived" -FROM ( - SELECT * FROM "JsonEntitiesInheritance" AS j -) AS m -"""); - } - - 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); - - AssertSql( - """ -SELECT m."Id", m."Discriminator", m."Name", m."Fraction", m."CollectionOnBase", m."ReferenceOnBase", m."CollectionOnDerived", m."ReferenceOnDerived" -FROM ( - SELECT * FROM "JsonEntitiesInheritance" AS j -) AS m -WHERE m."Discriminator" = 'JsonEntityInheritanceDerived' -"""); - } - - 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); - - AssertSql( - """ -SELECT m."ReferenceOnBase", m."Id" -FROM ( - SELECT * FROM "JsonEntitiesInheritance" AS j -) AS m -ORDER BY m."Id" NULLS FIRST -"""); - } - - 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); - - AssertSql( - """ -SELECT m."CollectionOnDerived", m."Id" -FROM ( - SELECT * FROM "JsonEntitiesInheritance" AS j -) AS m -WHERE m."Discriminator" = 'JsonEntityInheritanceDerived' -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" jsonb, - "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='1' - -SELECT j."Id", j."OwnedReferenceRoot" #> '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',0,'OwnedCollectionLeaf',@prm]::text[], @prm -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' -@prm2='1' - -SELECT j."Id", j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@prm1,'OwnedCollectionLeaf',@prm2]::text[], @prm1, @prm2 -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' - -SELECT j."Id", j."OwnedReferenceRoot", j."OwnedReferenceRoot" #> ARRAY['OwnedCollectionBranch',@prm,'OwnedCollectionLeaf',1]::text[], @prm -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()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - 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 is null, a is null); - - if (e is not null && a is not 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 is null, a is null); - - if (e is not null && a is not null) - { - var ee = (JsonOwnedAllTypes)e; - var aa = (JsonOwnedAllTypes)a; - - AssertAllTypes(ee, aa); - } - }; - - _entityAsserters = entityAsserters; - } - - IReadOnlyDictionary IQueryFixtureBase.EntityAsserters - => _entityAsserters; - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - { - base.ConfigureConventions(configurationBuilder); - - // The tests seed Unspecified DateTimes, but our default mapping for DateTime is timestamptz, which requires UTC. - // Map these properties to "timestamp without time zone". - configurationBuilder.Properties().HaveColumnType("timestamp without time zone"); - configurationBuilder.Properties>().HaveColumnType("timestamp without time zone[]"); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // The following are ignored since we do not support mapping IList (as opposed to array/List) on regular properties - // (since that's not supported at the ADO.NET layer). However, we do support IList inside JSON documents, since that doesn't - // rely on ADO.NET support. - modelBuilder.Entity( - b => - { - b.Ignore(j => j.TestEnumCollection); - b.Ignore(j => j.TestUnsignedInt16Collection); - b.Ignore(j => j.TestNullableEnumCollection); - b.Ignore(j => j.TestNullableEnumWithIntConverterCollection); - b.Ignore(j => j.TestCharacterCollection); - b.Ignore(j => j.TestNullableInt32Collection); - b.Ignore(j => j.TestUnsignedInt64Collection); - - b.Ignore(j => j.TestByteCollection); - b.Ignore(j => j.TestBooleanCollection); - b.Ignore(j => j.TestDateTimeOffsetCollection); - 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() - { - if (_expectedData is null) - { - _expectedData = (JsonQueryData)base.GetExpectedData(); - - // 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. - foreach (var j in _expectedData.JsonEntitiesAllTypes) - { - j.Reference.TestDateTimeOffset = new DateTimeOffset( - j.Reference.TestDateTimeOffset.Ticks - - (j.Reference.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - - foreach (var j2 in j.Collection) - { - j2.TestDateTimeOffset = new DateTimeOffset( - j2.TestDateTimeOffset.Ticks - (j2.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), - TimeSpan.Zero); - } - - j.TestDateTimeOffsetCollection = j.TestDateTimeOffsetCollection.Select( - dto => new DateTimeOffset(dto.Ticks - (dto.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero)).ToList(); - } - } - - return _expectedData; - } - - 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. - // See https://github.com/dotnet/efcore/issues/26068 - - var jsonEntitiesBasic = JsonQueryData.CreateJsonEntitiesBasic(); - var entitiesBasic = JsonQueryData.CreateEntitiesBasic(); - var jsonEntitiesBasicForReference = JsonQueryData.CreateJsonEntitiesBasicForReference(); - var jsonEntitiesBasicForCollection = JsonQueryData.CreateJsonEntitiesBasicForCollection(); - JsonQueryData.WireUp(jsonEntitiesBasic, entitiesBasic, jsonEntitiesBasicForReference, jsonEntitiesBasicForCollection); - - var jsonEntitiesCustomNaming = JsonQueryData.CreateJsonEntitiesCustomNaming(); - var jsonEntitiesSingleOwned = JsonQueryData.CreateJsonEntitiesSingleOwned(); - var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); - var jsonEntitiesAllTypes = JsonQueryData.CreateJsonEntitiesAllTypes(); - var jsonEntitiesConverters = JsonQueryData.CreateJsonEntitiesConverters(); - - context.JsonEntitiesBasic.AddRange(jsonEntitiesBasic); - context.EntitiesBasic.AddRange(entitiesBasic); - context.JsonEntitiesBasicForReference.AddRange(jsonEntitiesBasicForReference); - context.JsonEntitiesBasicForCollection.AddRange(jsonEntitiesBasicForCollection); - context.JsonEntitiesCustomNaming.AddRange(jsonEntitiesCustomNaming); - context.JsonEntitiesSingleOwned.AddRange(jsonEntitiesSingleOwned); - context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); - context.JsonEntitiesAllTypes.AddRange(jsonEntitiesAllTypes); - context.JsonEntitiesConverters.AddRange(jsonEntitiesConverters); - - foreach (var j in jsonEntitiesAllTypes) - { - j.Reference.TestDateTimeOffset = new DateTimeOffset( - j.Reference.TestDateTimeOffset.Ticks - (j.Reference.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), - TimeSpan.Zero); - - foreach (var j2 in j.Collection) - { - j2.TestDateTimeOffset = new DateTimeOffset( - j2.TestDateTimeOffset.Ticks - (j2.TestDateTimeOffset.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - } - - j.TestDateTimeOffsetCollection = j.TestDateTimeOffsetCollection.Select( - dto => new DateTimeOffset(dto.Ticks - (dto.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero)).ToList(); - } - - 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/LegacyNpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs deleted file mode 100644 index c5f228d856..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/LegacyNpgsqlNodaTimeTypeMappingTest.cs +++ /dev/null @@ -1,104 +0,0 @@ -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 Microsoft.EntityFrameworkCore.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/ManyToManyNoTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ManyToManyNoTrackingQueryNpgsqlTest.cs deleted file mode 100644 index efb6c3a87c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ManyToManyNoTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -internal class ManyToManyNoTrackingQueryNpgsqlTest - : ManyToManyNoTrackingQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public ManyToManyNoTrackingQueryNpgsqlTest(ManyToManyQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // TODO: #1232 - // protected override bool CanExecuteQueryString => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ManyToManyQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/ManyToManyQueryNpgsqlFixture.cs deleted file mode 100644 index 6eea1a665b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ManyToManyQueryNpgsqlFixture.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class ManyToManyQueryNpgsqlFixture : ManyToManyQueryRelationalFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. So we need to explicitly set some column types to 'timestamp without time zone', but this is difficult/problematic - // for some of the many-to-many join entities configured below. So for now we duplicate the entire method. - // TODO: https://github.com/dotnet/efcore/issues/26068 - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); - modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); - modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); - modelBuilder.Entity().HasKey( - e => new - { - e.Key1, - e.Key2, - e.Key3 - }); - - // Npgsql customization: - modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); - - modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); - modelBuilder.Entity().HasBaseType(); - modelBuilder.Entity().HasBaseType(); - - modelBuilder.Entity() - .HasMany(e => e.Collection) - .WithOne(e => e.CollectionInverse) - .HasForeignKey(e => e.CollectionInverseId); - - modelBuilder.Entity() - .HasOne(e => e.Reference) - .WithOne(e => e.ReferenceInverse) - .HasForeignKey(e => e.ReferenceInverseId); - - modelBuilder.Entity() - .HasMany(e => e.TwoSkipShared) - .WithMany(e => e.OneSkipShared); - - // Nav:2 Payload:No Join:Concrete Extra:None - modelBuilder.Entity() - .HasMany(e => e.TwoSkip) - .WithMany(e => e.OneSkip) - .UsingEntity(); - - // Nav:6 Payload:Yes Join:Concrete Extra:None - modelBuilder.Entity() - .HasMany(e => e.ThreeSkipPayloadFull) - .WithMany(e => e.OneSkipPayloadFull) - .UsingEntity( - r => r.HasOne(x => x.Three).WithMany(e => e.JoinOnePayloadFull), - l => l.HasOne(x => x.One).WithMany(e => e.JoinThreePayloadFull)); - - // Nav:4 Payload:Yes Join:Shared Extra:None - modelBuilder.Entity() - .HasMany(e => e.ThreeSkipPayloadFullShared) - .WithMany(e => e.OneSkipPayloadFullShared) - .UsingEntity>( - "JoinOneToThreePayloadFullShared", - r => r.HasOne().WithMany(e => e.JoinOnePayloadFullShared).HasForeignKey("ThreeId"), - l => l.HasOne().WithMany(e => e.JoinThreePayloadFullShared).HasForeignKey("OneId")) - .IndexerProperty("Payload"); - - // Nav:6 Payload:Yes Join:Concrete Extra:Self-Ref - modelBuilder.Entity() - .HasMany(e => e.SelfSkipPayloadLeft) - .WithMany(e => e.SelfSkipPayloadRight) - .UsingEntity( - l => l.HasOne(x => x.Left).WithMany(x => x.JoinSelfPayloadLeft), - r => r.HasOne(x => x.Right).WithMany(x => x.JoinSelfPayloadRight), - // Npgsql customization - x => x.Property(e => e.Payload).HasColumnType("timestamp without time zone")); - - // Nav:2 Payload:No Join:Concrete Extra:Inheritance - modelBuilder.Entity() - .HasMany(e => e.BranchSkip) - .WithMany(e => e.OneSkip) - .UsingEntity(); - - modelBuilder.Entity() - .HasOne(e => e.Reference) - .WithOne(e => e.ReferenceInverse) - .HasForeignKey(e => e.ReferenceInverseId); - - modelBuilder.Entity() - .HasMany(e => e.Collection) - .WithOne(e => e.CollectionInverse) - .HasForeignKey(e => e.CollectionInverseId); - - // Nav:6 Payload:No Join:Concrete Extra:None - modelBuilder.Entity() - .HasMany(e => e.ThreeSkipFull) - .WithMany(e => e.TwoSkipFull) - .UsingEntity( - r => r.HasOne(x => x.Three).WithMany(e => e.JoinTwoFull), - l => l.HasOne(x => x.Two).WithMany(e => e.JoinThreeFull)); - - // Nav:2 Payload:No Join:Shared Extra:Self-ref - modelBuilder.Entity() - .HasMany(e => e.SelfSkipSharedLeft) - .WithMany(e => e.SelfSkipSharedRight); - - // Nav:2 Payload:No Join:Shared Extra:CompositeKey - modelBuilder.Entity() - .HasMany(e => e.CompositeKeySkipShared) - .WithMany(e => e.TwoSkipShared); - - // Nav:6 Payload:No Join:Concrete Extra:CompositeKey - modelBuilder.Entity() - .HasMany(e => e.CompositeKeySkipFull) - .WithMany(e => e.ThreeSkipFull) - .UsingEntity( - l => l.HasOne(x => x.Composite).WithMany(x => x.JoinThreeFull).HasForeignKey( - e => new - { - e.CompositeId1, - e.CompositeId2, - e.CompositeId3 - }).IsRequired(), - r => r.HasOne(x => x.Three).WithMany(x => x.JoinCompositeKeyFull).IsRequired()); - - // Nav:2 Payload:No Join:Shared Extra:Inheritance - modelBuilder.Entity() - .HasMany(e => e.RootSkipShared) - .WithMany(e => e.ThreeSkipShared); - - // Nav:2 Payload:No Join:Shared Extra:Inheritance,CompositeKey - modelBuilder.Entity() - .HasMany(e => e.RootSkipShared) - .WithMany(e => e.CompositeKeySkipShared); - - // Nav:6 Payload:No Join:Concrete Extra:Inheritance,CompositeKey - modelBuilder.Entity() - .HasMany(e => e.LeafSkipFull) - .WithMany(e => e.CompositeKeySkipFull) - .UsingEntity( - r => r.HasOne(x => x.Leaf).WithMany(x => x.JoinCompositeKeyFull), - l => l.HasOne(x => x.Composite).WithMany(x => x.JoinLeafFull).HasForeignKey( - e => new - { - e.CompositeId1, - e.CompositeId2, - e.CompositeId3 - }), - x => - { - // Npgsql customization - x.Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - }); - - modelBuilder.SharedTypeEntity( - "PST", b => - { - b.IndexerProperty("Id").ValueGeneratedNever(); - b.IndexerProperty("Payload"); - }); - } - - // protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - // { - // base.OnModelCreating(modelBuilder, context); - // - // // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // // supported. - // modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); - // modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - // modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); - // modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - // } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ManyToManyQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ManyToManyQueryNpgsqlTest.cs deleted file mode 100644 index 6a1ee1b528..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ManyToManyQueryNpgsqlTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -internal class ManyToManyQueryNpgsqlTest : ManyToManyQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public ManyToManyQueryNpgsqlTest(ManyToManyQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/MappingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/MappingQueryNpgsqlTest.cs deleted file mode 100644 index dbe42cee60..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/MappingQueryNpgsqlTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -// ReSharper disable once UnusedMember.Global -public class MappingQueryNpgsqlTest : MappingQueryTestBase -{ - public MappingQueryNpgsqlTest(MappingQueryNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public class MappingQueryNpgsqlFixture : MappingQueryFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlNorthwindTestStoreFactory.Instance; - - protected override string DatabaseSchema { get; } = "public"; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity( - e => - { - e.Property(c => c.CompanyName2).Metadata.SetColumnName("CompanyName"); - e.Metadata.SetTableName("Customers"); - e.Metadata.SetSchema("public"); - }); - - modelBuilder.Entity() - .Property(c => c.EmployeeID) - .HasColumnType("int"); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs deleted file mode 100644 index 5ea8bc4860..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NonSharedPrimitiveCollectionsQueryNpgsqlTest.cs +++ /dev/null @@ -1,169 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NonSharedPrimitiveCollectionsQueryNpgsqlTest(NonSharedFixture fixture) - : NonSharedPrimitiveCollectionsQueryRelationalTestBase(fixture) -{ - protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode) - { - new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); - - return optionsBuilder; - } - - #region Support for specific element types - - // Since we just use arrays for primitive collections, there's no need to test each and every element type; arrays are fully typed - // and don't need any special conversion/handling like in providers which use JSON. - - // Npgsql maps DateTime to timestamp with time zone by default, which requires UTC timestamps. - public override Task Array_of_DateTime() - => TestArray( - new DateTime(2023, 1, 1, 12, 30, 0), - new DateTime(2023, 1, 2, 12, 30, 0), - mb => mb.Entity() - .Property(typeof(DateTime[]), "SomeArray") - .HasColumnType("timestamp without time zone[]")); - - // Npgsql maps DateTime to timestamp with time zone by default, which requires UTC timestamps. - public override Task Array_of_DateTime_with_milliseconds() - => TestArray( - new DateTime(2023, 1, 1, 12, 30, 0, 123), - new DateTime(2023, 1, 1, 12, 30, 0, 124), - mb => mb.Entity() - .Property(typeof(DateTime[]), "SomeArray") - .HasColumnType("timestamp without time zone[]")); - - // Npgsql maps DateTime to timestamp with time zone by default, which requires UTC timestamps. - public override Task Array_of_DateTime_with_microseconds() - => TestArray( - new DateTime(2023, 1, 1, 12, 30, 0, 123, 456), - new DateTime(2023, 1, 1, 12, 30, 0, 123, 457), - mb => mb.Entity() - .Property(typeof(DateTime[]), "SomeArray") - .HasColumnType("timestamp without time zone[]")); - - [ConditionalFact] - public virtual Task Array_of_DateTime_utc() - => TestArray( - new DateTime(2023, 1, 1, 12, 30, 0, DateTimeKind.Utc), - new DateTime(2023, 1, 2, 12, 30, 0, DateTimeKind.Utc)); - - // Npgsql only supports DateTimeOffset with Offset 0 (mapped to timestamp with time zone) - public override Task Array_of_DateTimeOffset() - => TestArray( - new DateTimeOffset(2023, 1, 1, 12, 30, 0, TimeSpan.Zero), - new DateTimeOffset(2023, 1, 2, 12, 30, 0, TimeSpan.Zero)); - - [ConditionalFact] - public override async Task Multidimensional_array_is_not_supported() - { - // Multidimensional arrays are supported in PostgreSQL (via the regular array type); the EFCore.PG maps .NET - // multidimensional arrays. However, arrays of multidimensional arrays aren't supported (since arrays of arrays generally aren't - // supported). - var contextFactory = await InitializeAsync( - mb => mb.Entity().Property("MultidimensionalArray"), - seed: async context => - { - var entry = context.Add(new TestEntity()); - entry.Property("MultidimensionalArray").CurrentValue = new[,] { { 1, 2 }, { 3, 4 } }; - await context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateContext(); - - var arrays = new[] { new[,] { { 1, 2 }, { 3, 4 } }, new[,] { { 1, 2 }, { 3, 5 } } }; - - await Assert.ThrowsAsync( - () => - context.Set().Where(t => arrays.Contains(EF.Property(t, "MultidimensionalArray"))).ToArrayAsync()); - } - - #endregion Support for specific element types - - public override async Task 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 jsonb_array_length(t."Owned" -> 'Strings') = 2 -LIMIT 2 -""", - // - """ -SELECT t."Id", t."Owned" -FROM "TestOwner" AS t -WHERE (t."Owned" #>> '{Strings,1}') = 'bar' -LIMIT 2 -"""); - } - - #region Contains with various index methods - - // For Contains over column collections that have a (modeled) GIN index, we translate to the containment operator (@>). - // Otherwise we translate to the ANY construct. - [ConditionalFact] - public virtual async Task Column_collection_Contains_with_GIN_index_uses_containment() - { - var contextFactory = await InitializeAsync( - onModelCreating: mb => mb.Entity() - .HasIndex(e => e.Ints) - .HasMethod("GIN"), - seed: context => - { - context.AddRange( - new TestEntity { Id = 1, Ints = [1, 2, 3] }, - new TestEntity { Id = 2, Ints = [1, 2, 4] }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateContext(); - - var result = await context.Set().Where(c => c.Ints!.Contains(4)).SingleAsync(); - Assert.Equal(2, result.Id); - - AssertSql( - """ -SELECT t."Id", t."Ints" -FROM "TestEntity" AS t -WHERE t."Ints" @> ARRAY[4]::integer[] -LIMIT 2 -"""); - } - - [ConditionalFact] - public virtual async Task Column_collection_Contains_with_btree_index_does_not_use_containment() - { - var contextFactory = await InitializeAsync( - onModelCreating: mb => mb.Entity().HasIndex(e => e.Ints), - seed: context => - { - context.AddRange( - new TestEntity { Id = 1, Ints = [1, 2, 3] }, - new TestEntity { Id = 2, Ints = [1, 2, 4] }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateContext(); - - var result = await context.Set().Where(c => c.Ints!.Contains(4)).SingleAsync(); - Assert.Equal(2, result.Id); - - AssertSql( - """ -SELECT t."Id", t."Ints" -FROM "TestEntity" AS t -WHERE 4 = ANY (t."Ints") -LIMIT 2 -"""); - } - - #endregion Contains with various index methods - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs deleted file mode 100644 index 12965bffe1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindAggregateOperatorsQueryNpgsqlTest.cs +++ /dev/null @@ -1,181 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindAggregateOperatorsQueryNpgsqlTest : NorthwindAggregateOperatorsQueryRelationalTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindAggregateOperatorsQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // Overriding to add equality tolerance because of floating point precision - 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: (e, a) => Assert.Equal(e, a, 10)); - - AssertSql( - """ -@p='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 -) AS c0 -"""); - } - - public override async Task Contains_with_local_uint_array_closure(bool async) - { - await base.Contains_with_local_uint_array_closure(async); - - // Note: PostgreSQL doesn't support uint, but value converters make this into bigint - AssertSql( - """ -@ids={ '0' -'1' } (DbType = Object) - -SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" -FROM "Employees" AS e -WHERE e."EmployeeID" = ANY (@ids) -""", - // - """ -@ids={ '0' } (DbType = Object) - -SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" -FROM "Employees" AS e -WHERE e."EmployeeID" = ANY (@ids) -"""); - } - - public override async Task Contains_with_local_nullable_uint_array_closure(bool async) - { - await base.Contains_with_local_nullable_uint_array_closure(async); - - // Note: PostgreSQL doesn't support uint, but value converters make this into bigint - - AssertSql( - """ -@ids={ '0' -'1' } (DbType = Object) - -SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" -FROM "Employees" AS e -WHERE e."EmployeeID" = ANY (@ids) -""", - // - """ -@ids={ '0' } (DbType = Object) - -SELECT e."EmployeeID", e."City", e."Country", e."FirstName", e."ReportsTo", e."Title" -FROM "Employees" AS e -WHERE e."EmployeeID" = ANY (@ids) -"""); - } - - public override Task Contains_with_local_anonymous_type_array_closure(bool async) - // Aggregates. Issue #15937. - => AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); - - public override Task Contains_with_local_tuple_array_closure(bool async) - => Assert.ThrowsAsync(() => base.Contains_with_local_tuple_array_closure(async: true)); - - public override async Task Contains_with_local_enumerable_inline(bool async) - { - // Issue #31776 - await Assert.ThrowsAsync( - async () => - await base.Contains_with_local_enumerable_inline(async)); - - AssertSql(); - } - - public override async Task Contains_with_local_enumerable_inline_closure_mix(bool async) - { - await base.Contains_with_local_enumerable_inline_closure_mix(async); - - AssertSql( - """ -@p={ '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, NULL)) -""", - // - """ -@p={ '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, NULL)) -"""); - } - - public override async Task Contains_with_local_non_primitive_list_closure_mix(bool async) - { - await base.Contains_with_local_non_primitive_list_closure_mix(async); - - AssertSql( - """ -@Select={ '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 (@Select) -"""); - } - - public override async Task Contains_with_local_non_primitive_list_inline_closure_mix(bool async) - { - await base.Contains_with_local_non_primitive_list_inline_closure_mix(async); - - AssertSql( - """ -@Select={ '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 (@Select) -""", - // - """ -@Select={ '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 (@Select) -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindAsNoTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindAsNoTrackingQueryNpgsqlTest.cs deleted file mode 100644 index e16b0990df..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindAsNoTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindAsNoTrackingQueryNpgsqlTest : NorthwindAsNoTrackingQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindAsNoTrackingQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindAsTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindAsTrackingQueryNpgsqlTest.cs deleted file mode 100644 index 68f5b20197..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindAsTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindAsTrackingQueryNpgsqlTest : NorthwindAsTrackingQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindAsTrackingQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindChangeTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindChangeTrackingQueryNpgsqlTest.cs deleted file mode 100644 index 2a39e1d16b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindChangeTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindChangeTrackingQueryNpgsqlTest : NorthwindChangeTrackingQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindChangeTrackingQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - protected override NorthwindContext CreateNoTrackingContext() - => new NorthwindNpgsqlContext( - new DbContextOptionsBuilder(Fixture.CreateOptions()) - .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking).Options); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs deleted file mode 100644 index ba9d574530..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindCompiledQueryNpgsqlTest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindCompiledQueryNpgsqlTest : NorthwindCompiledQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindCompiledQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs deleted file mode 100644 index b6116abf2b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs +++ /dev/null @@ -1,263 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class NorthwindDbFunctionsQueryNpgsqlTest : NorthwindDbFunctionsQueryRelationalTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindDbFunctionsQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Like / ILike - - public override async Task Like_literal(bool async) - { - // PostgreSQL like is case-sensitive, while the EF Core "default" (i.e. SqlServer) is insensitive. - // So we override and assert only 19 matches unlike the default's 34. - await AssertCount( - async, - ss => ss.Set(), - ss => ss.Set(), - c => EF.Functions.Like(c.ContactName, "%M%"), - c => c.ContactName.Contains("M")); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" LIKE '%M%' -"""); - } - - public override async Task Like_identity(bool async) - { - await base.Like_identity(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" LIKE c."ContactName" ESCAPE '' -"""); - } - - public override async Task Like_literal_with_escape(bool async) - { - await base.Like_literal_with_escape(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" LIKE '!%' ESCAPE '!' -"""); - } - - [Fact] - public void String_Like_Literal_With_Backslash() - { - using var context = CreateContext(); - var count = context.Customers.Count(c => EF.Functions.Like(c.ContactName, "\\")); - - Assert.Equal(0, count); - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" LIKE '\' ESCAPE '' -"""); - } - - [Fact] - public void String_ILike_Literal() - { - using var context = CreateContext(); - var count = context.Customers.Count(c => EF.Functions.ILike(c.ContactName, "%M%")); - - Assert.Equal(34, count); - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" ILIKE '%M%' -"""); - } - - [Fact] - public void String_ILike_Literal_With_Escape() - { - using var context = CreateContext(); - var count = context.Customers.Count(c => EF.Functions.ILike(c.ContactName, "!%", "!")); - - Assert.Equal(0, count); - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -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 - - [MinimumPostgresVersion(12, 0)] - [PlatformSkipCondition(TestUtilities.Xunit.TestPlatform.Windows, SkipReason = "ICU non-deterministic doesn't seem to work on Windows?")] - public override async Task Collate_case_insensitive(bool async) - { - await base.Collate_case_insensitive(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" COLLATE "some-case-insensitive-collation" = 'maria anders' -"""); - } - - public override async Task Collate_case_sensitive(bool async) - { - await base.Collate_case_sensitive(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE c."ContactName" COLLATE "POSIX" = 'maria anders' -"""); - } - - protected override string CaseInsensitiveCollation - => "some-case-insensitive-collation"; - - protected override string CaseSensitiveCollation - => "POSIX"; - - #endregion Collation - - #region Others - - [Fact] - public void Distance_with_timestamp() - { - using var context = CreateContext(); - var closestOrder = context.Orders.OrderBy(o => EF.Functions.Distance(o.OrderDate.Value, new DateTime(1997, 06, 28))).First(); - - Assert.Equal(10582, closestOrder.OrderID); - - AssertSql( - """ -SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Orders" AS o -ORDER BY o."OrderDate" <-> TIMESTAMP '1997-06-28T00:00:00' NULLS FIRST -LIMIT 1 -"""); - } - - [Fact] - public void String_reverse() - { - using var context = CreateContext(); - var count = context.Customers.Count(c => EF.Functions.Reverse(c.ContactName) == "srednA airaM"); - - Assert.Equal(1, count); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE reverse(c."ContactName") = 'srednA airaM' -"""); - } - - // ReSharper disable once InconsistentNaming - [Fact] - public void StringToArray() - { - using var context = CreateContext(); - var count = context.Customers.Count(c => EF.Functions.StringToArray(c.ContactName, " ") == new[] { "Maria", "Anders" }); - - Assert.Equal(1, count); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE string_to_array(c."ContactName", ' ') = ARRAY['Maria','Anders']::text[] -"""); - } - - [Fact] - public void StringToArray_with_null_string() - { - using var context = CreateContext(); - var count = context.Customers.Count(c => EF.Functions.StringToArray(c.ContactName, " ", "Maria") == new[] { null, "Anders" }); - - Assert.Equal(1, count); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE string_to_array(c."ContactName", ' ', 'Maria') = ARRAY[NULL,'Anders']::text[] -"""); - } - - [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) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryNpgsqlTest.cs deleted file mode 100644 index 576cfdf768..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryNpgsqlTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindEFPropertyIncludeQueryNpgsqlTest : NorthwindEFPropertyIncludeQueryTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindEFPropertyIncludeQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public override async Task Include_collection_with_last_no_orderby(bool async) - { - Assert.Equal( - RelationalStrings.LastUsedWithoutOrderBy(nameof(Enumerable.Last)), - (await Assert.ThrowsAsync( - () => base.Include_collection_with_last_no_orderby(async))).Message); - - AssertSql(); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs deleted file mode 100644 index fe360fda2d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs +++ /dev/null @@ -1,1062 +0,0 @@ -using System.Text.Json; -using System.Text.RegularExpressions; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class NorthwindFunctionsQueryNpgsqlTest : NorthwindFunctionsQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindFunctionsQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Substring - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task Substring_without_length_with_Index_of(bool async) - => AssertQuery( - async, - ss => ss.Set() - .Where(x => x.Address == "Walserweg 21") - .Where(x => x.Address.Substring(x.Address.IndexOf("e")) == "erweg 21")); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task Substring_without_length_with_constant(bool async) - => AssertQuery( - async, - //Walserweg 21 - cs => cs.Set().Where(x => x.Address.Substring(5) == "rweg 21")); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task Substring_without_length_with_closure(bool async) - { - var startIndex = 5; - return AssertQuery( - async, - //Walserweg 21 - ss => ss.Set().Where(x => x.Address.Substring(startIndex) == "rweg 21")); - } - - #endregion - - #region Regex - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_IsMatch_with_constant_pattern(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_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) - { - var pattern = "^A"; - - await AssertQuery( - async, - cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, pattern))); - - AssertSql( - """ -@pattern='^A' - -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)' || @pattern) -"""); - } - - [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) - { - await AssertQuery( - async, - cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.None))); - - 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_IgnoreCase(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_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) - { - await AssertQuery( - async, - cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.Multiline))); - - 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" ~ '(?n)^A' -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_IsMatch_with_Singleline(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.Singleline))); - - 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" ~ '^A' -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_IsMatch_with_Singleline_and_IgnoreCase(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^a", RegexOptions.Singleline | 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" ~* '^a' -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_IsMatch_with_IgnorePatternWhitespace(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Where(c => Regex.IsMatch(c.CompanyName, "^ A", RegexOptions.IgnorePatternWhitespace))); - - 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" ~ '(?px)^ A' -"""); - } - - [Fact] - public void Regex_IsMatch_with_unsupported_option() - => Assert.Throws( - () => - Fixture.CreateContext().Customers.Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.RightToLeft)).ToList()); - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_constant_pattern_and_replacement(bool async) - { - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B"))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') -FROM "Customers" AS c -""" - ); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_parameter_pattern_and_replacement(bool async) - { - var pattern = "^A"; - var replacement = "B"; - - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, pattern, replacement))); - - AssertSql( - """ -@pattern='^A' -@replacement='B' - -SELECT regexp_replace(c."CompanyName", @pattern, @replacement, 'p') -FROM "Customers" AS c -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_OptionsNone(bool async) - { - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.None))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') -FROM "Customers" AS c -""" - ); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_IgnoreCase(bool async) - { - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.IgnoreCase))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^a', 'B', 'pi') -FROM "Customers" AS c -""" - ); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_Multiline(bool async) - { - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.Multiline))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^A', 'B', 'n') -FROM "Customers" AS c -""" - ); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_Singleline(bool async) - { - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.Singleline))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^A', 'B') -FROM "Customers" AS c -""" - ); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_Singleline_and_IgnoreCase(bool async) - { - await AssertQuery( - async, - source => source.Set() - .Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.Singleline | RegexOptions.IgnoreCase))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^a', 'B', 'i') -FROM "Customers" AS c -""" - ); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Regex_Replace_with_IgnorePatternWhitespace(bool async) - { - await AssertQuery( - async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^ A", "B", RegexOptions.IgnorePatternWhitespace))); - - AssertSql( - """ -SELECT regexp_replace(c."CompanyName", '^ A', 'B', 'px') -FROM "Customers" AS c -""" - ); - } - - [Fact] - public void Regex_Replace_with_unsupported_option() - => Assert.Throws( - () => Fixture.CreateContext().Customers - .FirstOrDefault(x => Regex.Replace(x.CompanyName, "^A", "foo", RegexOptions.RightToLeft) != null)); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_constant_pattern(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A"))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^A', 1, 'p') -FROM "Customers" AS c -""" - ); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_parameter_pattern(bool async) - { - var pattern = "^A"; - - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, pattern))); - - AssertSql( - """ -@pattern='^A' - -SELECT regexp_count(c."CompanyName", @pattern, 1, 'p') -FROM "Customers" AS c -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_OptionsNone(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.None))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^A', 1, 'p') -FROM "Customers" AS c -""" - ); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_IgnoreCase(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^a", RegexOptions.IgnoreCase))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^a', 1, 'pi') -FROM "Customers" AS c -""" - ); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_Multiline(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.Multiline))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^A', 1, 'n') -FROM "Customers" AS c -""" - ); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_Singleline(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.Singleline))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^A') -FROM "Customers" AS c -""" - ); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_Singleline_and_IgnoreCase(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^a", RegexOptions.Singleline | RegexOptions.IgnoreCase))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^a', 1, 'i') -FROM "Customers" AS c -""" - ); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - [MinimumPostgresVersion(15, 0)] - public async Task Regex_Count_with_IgnorePatternWhitespace(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^ A", RegexOptions.IgnorePatternWhitespace))); - - AssertSql( - """ -SELECT regexp_count(c."CompanyName", '^ A', 1, 'px') -FROM "Customers" AS c -""" - ); - } - - [ConditionalFact] - [MinimumPostgresVersion(15, 0)] - public void Regex_Count_with_unsupported_option() - => Assert.Throws( - () => Fixture.CreateContext().Customers - .FirstOrDefault(x => Regex.Count(x.CompanyName, "^A", RegexOptions.RightToLeft) != 0)); - - #endregion Regex - - #region PadLeft, PadRight - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadLeft_with_constant(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadLeft(20).EndsWith("Walserweg 21"))); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadLeft_char_with_constant(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadLeft(20, 'a').EndsWith("Walserweg 21"))); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadLeft_with_parameter(bool async) - { - var length = 20; - - return AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadLeft(length).EndsWith("Walserweg 21"))); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadLeft_char_with_parameter(bool async) - { - var length = 20; - - return AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadLeft(length, 'a').EndsWith("Walserweg 21"))); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadRight_with_constant(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadRight(20).StartsWith("Walserweg 21"))); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadRight_char_with_constant(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadRight(20).StartsWith("Walserweg 21"))); - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadRight_with_parameter(bool async) - { - var length = 20; - - return AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadRight(length).StartsWith("Walserweg 21"))); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public Task PadRight_char_with_parameter(bool async) - { - var length = 20; - - return AssertQuery( - async, - ss => ss.Set().Where(x => x.Address.PadRight(length, 'a').StartsWith("Walserweg 21"))); - } - - #endregion - - #region Aggregate functions - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_ArrayAgg(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(c => c.City) - .Select(g => new { City = g.Key, FaxNumbers = EF.Functions.ArrayAgg(g.Select(c => c.Fax).OrderBy(id => id)) }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var london = results.Single(x => x.City == "London"); - Assert.Collection( - london.FaxNumbers, - Assert.Null, - f => Assert.Equal("(171) 555-2530", f), - f => Assert.Equal("(171) 555-3373", f), - f => Assert.Equal("(171) 555-5646", f), - f => Assert.Equal("(171) 555-6750", f), - f => Assert.Equal("(171) 555-9199", f)); - - AssertSql( - """ -SELECT c."City", array_agg(c."Fax" ORDER BY c."Fax" NULLS FIRST) AS "FaxNumbers" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_JsonAgg(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(c => c.City) - .Select(g => new { City = g.Key, FaxNumbers = EF.Functions.JsonAgg(g.Select(c => c.Fax).OrderBy(id => id)) }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var london = results.Single(x => x.City == "London"); - Assert.Collection( - london.FaxNumbers, - Assert.Null, - f => Assert.Equal("(171) 555-2530", f), - f => Assert.Equal("(171) 555-3373", f), - f => Assert.Equal("(171) 555-5646", f), - f => Assert.Equal("(171) 555-6750", f), - f => Assert.Equal("(171) 555-9199", f)); - - AssertSql( - """ -SELECT c."City", json_agg(c."Fax" ORDER BY c."Fax" NULLS FIRST) AS "FaxNumbers" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_JsonbAgg(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(c => c.City) - .Select(g => new { City = g.Key, FaxNumbers = EF.Functions.JsonbAgg(g.Select(c => c.Fax).OrderBy(id => id)) }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var london = results.Single(x => x.City == "London"); - Assert.Collection( - london.FaxNumbers, - Assert.Null, - f => Assert.Equal("(171) 555-2530", f), - f => Assert.Equal("(171) 555-3373", f), - f => Assert.Equal("(171) 555-5646", f), - f => Assert.Equal("(171) 555-6750", f), - f => Assert.Equal("(171) 555-9199", f)); - - AssertSql( - """ -SELECT c."City", jsonb_agg(c."Fax" ORDER BY c."Fax" NULLS FIRST) AS "FaxNumbers" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - #endregion Aggregate functions - - #region JsonObjectAgg - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_JsonObjectAgg(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(c => c.City) - .Select( - g => new - { - City = g.Key, - Companies = EF.Functions.JsonObjectAgg( - g - .OrderBy(c => c.CompanyName) - .Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - 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" }""", - london.Companies); - - AssertSql( - """ -SELECT c."City", json_object_agg(c."CompanyName", c."ContactName" ORDER BY c."CompanyName" NULLS FIRST) AS "Companies" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_JsonObjectAgg_as_Dictionary(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(c => c.City) - .Select( - g => new - { - City = g.Key, - Companies = EF.Functions.JsonObjectAgg>( - g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var london = results.Single(r => r.City == "London"); - - Assert.Equal( - new Dictionary - { - ["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( - """ -SELECT c."City", json_object_agg(c."CompanyName", c."ContactName") AS "Companies" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_JsonbObjectAgg(bool async) - { - await using var ctx = CreateContext(); - - // Note that unlike with json, jsonb doesn't guarantee ordering; so we parse the JSON string client-side. - var query = ctx.Set() - .GroupBy(c => c.City) - .Select( - g => new - { - City = g.Key, - Companies = EF.Functions.JsonbObjectAgg(g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var london = results.Single(r => r.City == "London"); - var companiesDictionary = JsonSerializer.Deserialize>(london.Companies); - - Assert.Equal( - new Dictionary - { - ["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" - }, - companiesDictionary); - - AssertSql( - """ -SELECT c."City", jsonb_object_agg(c."CompanyName", c."ContactName") AS "Companies" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_JsonbObjectAgg_as_Dictionary(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(c => c.City) - .Select( - g => new - { - City = g.Key, - Companies = EF.Functions.JsonbObjectAgg>( - g.Select(c => ValueTuple.Create(c.CompanyName, c.ContactName))) - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var london = results.Single(r => r.City == "London"); - - Assert.Equal( - new Dictionary - { - ["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( - """ -SELECT c."City", jsonb_object_agg(c."CompanyName", c."ContactName") AS "Companies" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - #endregion JsonObjectAgg - - #region Statistics - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task StandardDeviation(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(od => od.ProductID) - .Select( - g => new - { - ProductID = g.Key, - SampleStandardDeviation = EF.Functions.StandardDeviationSample(g.Select(od => od.UnitPrice)), - PopulationStandardDeviation = EF.Functions.StandardDeviationPopulation(g.Select(od => od.UnitPrice)) - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var product9 = results.Single(r => r.ProductID == 9); - Assert.Equal(8.675943752699023, product9.SampleStandardDeviation.Value, 5); - Assert.Equal(7.759999999999856, product9.PopulationStandardDeviation.Value, 5); - - AssertSql( - """ -SELECT o."ProductID", stddev_samp(o."UnitPrice") AS "SampleStandardDeviation", stddev_pop(o."UnitPrice") AS "PopulationStandardDeviation" -FROM "Order Details" AS o -GROUP BY o."ProductID" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Variance(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(od => od.ProductID) - .Select( - g => new - { - ProductID = g.Key, - SampleStandardDeviation = EF.Functions.VarianceSample(g.Select(od => od.UnitPrice)), - PopulationStandardDeviation = EF.Functions.VariancePopulation(g.Select(od => od.UnitPrice)) - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var product9 = results.Single(r => r.ProductID == 9); - Assert.Equal(75.2719999999972, product9.SampleStandardDeviation.Value, 5); - Assert.Equal(60.217599999997766, product9.PopulationStandardDeviation.Value, 5); - - AssertSql( - """ -SELECT o."ProductID", var_samp(o."UnitPrice") AS "SampleStandardDeviation", var_pop(o."UnitPrice") AS "PopulationStandardDeviation" -FROM "Order Details" AS o -GROUP BY o."ProductID" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Other_statistics_functions(bool async) - { - await using var ctx = CreateContext(); - - var query = ctx.Set() - .GroupBy(od => od.ProductID) - .Select( - g => new - { - ProductID = g.Key, - Correlation = EF.Functions.Correlation(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - CovariancePopulation = - EF.Functions.CovariancePopulation(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - CovarianceSample = - EF.Functions.CovarianceSample(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrAverageX = EF.Functions.RegrAverageX(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrAverageY = EF.Functions.RegrAverageY(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrCount = EF.Functions.RegrCount(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrIntercept = EF.Functions.RegrIntercept(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrR2 = EF.Functions.RegrR2(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrSlope = EF.Functions.RegrSlope(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrSXX = EF.Functions.RegrSXX(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - RegrSXY = EF.Functions.RegrSXY(g.Select(od => ValueTuple.Create((double)od.Quantity, (double)od.Discount))), - }); - - var results = async - ? await query.ToListAsync() - : query.ToList(); - - var product9 = results.Single(r => r.ProductID == 9); - Assert.Equal(0.9336470941441423, product9.Correlation.Value, 5); - Assert.Equal(1.4799999967217445, product9.CovariancePopulation.Value, 5); - Assert.Equal(1.8499999959021807, product9.CovarianceSample.Value, 5); - Assert.Equal(0.10000000149011612, product9.RegrAverageX.Value, 5); - Assert.Equal(19, product9.RegrAverageY.Value, 5); - Assert.Equal(5, product9.RegrCount.Value); - Assert.Equal(2.5555555647538144, product9.RegrIntercept.Value, 5); - Assert.Equal(0.871696896403801, product9.RegrR2.Value, 5); - Assert.Equal(164.44444190204874, product9.RegrSlope.Value, 5); - Assert.Equal(0.045000000596046474, product9.RegrSXX.Value, 5); - Assert.Equal(7.399999983608723, product9.RegrSXY.Value, 5); - - AssertSql( - """ -SELECT o."ProductID", corr(o."Quantity"::double precision, o."Discount"::double precision) AS "Correlation", covar_pop(o."Quantity"::double precision, o."Discount"::double precision) AS "CovariancePopulation", covar_samp(o."Quantity"::double precision, o."Discount"::double precision) AS "CovarianceSample", regr_avgx(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrAverageX", regr_avgy(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrAverageY", regr_count(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrCount", regr_intercept(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrIntercept", regr_r2(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrR2", regr_slope(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrSlope", regr_sxx(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrSXX", regr_sxy(o."Quantity"::double precision, o."Discount"::double precision) AS "RegrSXY" -FROM "Order Details" AS o -GROUP BY o."ProductID" -"""); - } - - #endregion Statistics - - #region NullIf - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task NullIf_with_equality_left_sided(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(x => x.OrderID == 1 ? (int?)null : x.OrderID)); - - AssertSql( - """ -SELECT NULLIF(o."OrderID", 1) -FROM "Orders" AS o -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task NullIf_with_equality_right_sided(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(x => 1 == x.OrderID ? (int?)null : x.OrderID)); - - AssertSql( - """ -SELECT NULLIF(o."OrderID", 1) -FROM "Orders" AS o -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task NullIf_with_inequality_left_sided(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(x => x.OrderID != 1 ? x.OrderID : (int?)null)); - - AssertSql( - """ -SELECT NULLIF(o."OrderID", 1) -FROM "Orders" AS o -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task NullIf_with_inequality_right_sided(bool async) - { - await AssertQuery( - async, - cs => cs.Set().Select(x => 1 != x.OrderID ? x.OrderID : (int?)null)); - - AssertSql( - """ -SELECT NULLIF(o."OrderID", 1) -FROM "Orders" AS o -"""); - } - - #endregion - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs deleted file mode 100644 index 732bff2e0a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindGroupByQueryNpgsqlTest.cs +++ /dev/null @@ -1,3889 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindGroupByQueryNpgsqlTest : NorthwindGroupByQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindGroupByQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - #region GroupByProperty - - public override async Task GroupBy_Property_Select_Average(bool async) - { - await base.GroupBy_Property_Select_Average(async); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - - // Validating that we don't generate warning when translating GroupBy. See Issue#11157 - Assert.DoesNotContain( - "The LINQ expression 'GroupBy([o].CustomerID, [o])' could not be translated and will be evaluated locally.", - Fixture.TestSqlLoggerFactory.Log.Select(l => l.Message)); - } - - public override async Task GroupBy_Property_Select_Average_with_group_enumerable_projected(bool async) - { - await base.GroupBy_Property_Select_Average_with_group_enumerable_projected(async); - - AssertSql(); - } - - public override async Task GroupBy_Property_Select_Count(bool async) - { - await base.GroupBy_Property_Select_Count(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_LongCount(bool async) - { - await base.GroupBy_Property_Select_LongCount(async); - - AssertSql( - """ -SELECT count(*) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Count_with_nulls(bool async) - { - await base.GroupBy_Property_Select_Count_with_nulls(async); - - AssertSql( - """ -SELECT c."City", count(*)::int AS "Faxes" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - public override async Task GroupBy_Property_Select_LongCount_with_nulls(bool async) - { - await base.GroupBy_Property_Select_LongCount_with_nulls(async); - - AssertSql( - """ -SELECT c."City", count(*) AS "Faxes" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - public override async Task GroupBy_Property_Select_Max(bool async) - { - await base.GroupBy_Property_Select_Max(async); - - AssertSql( - """ -SELECT max(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Min(bool async) - { - await base.GroupBy_Property_Select_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Sum(bool async) - { - await base.GroupBy_Property_Select_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_Property_Select_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_Average(bool async) - { - await base.GroupBy_Property_Select_Key_Average(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Average" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_Count(bool async) - { - await base.GroupBy_Property_Select_Key_Count(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_LongCount(bool async) - { - await base.GroupBy_Property_Select_Key_LongCount(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*) AS "LongCount" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_Max(bool async) - { - await base.GroupBy_Property_Select_Key_Max(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", max(o."OrderID") AS "Max" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_Min(bool async) - { - await base.GroupBy_Property_Select_Key_Min(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", min(o."OrderID") AS "Min" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_Sum(bool async) - { - await base.GroupBy_Property_Select_Key_Sum(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_Property_Select_Key_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_Property_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID" AS "Key", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_key_multiple_times_and_aggregate(bool async) - { - await base.GroupBy_Property_Select_key_multiple_times_and_aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key1", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Key_with_constant(bool async) - { - await base.GroupBy_Property_Select_Key_with_constant(async); - - AssertSql( - """ -SELECT o0."Name", o0."CustomerID" AS "Value", count(*)::int AS "Count" -FROM ( - SELECT o."CustomerID", 'CustomerID' AS "Name" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Name", o0."CustomerID" -"""); - } - - public override async Task GroupBy_aggregate_projecting_conditional_expression(bool async) - { - await base.GroupBy_aggregate_projecting_conditional_expression(async); - - AssertSql( - """ -SELECT o."OrderDate" AS "Key", CASE - WHEN count(*)::int = 0 THEN 1 - ELSE COALESCE(sum(CASE - WHEN o."OrderID" % 2 = 0 THEN 1 - ELSE 0 - END), 0)::int / count(*)::int -END AS "SomeValue" -FROM "Orders" AS o -GROUP BY o."OrderDate" -"""); - } - - public override async Task GroupBy_aggregate_projecting_conditional_expression_based_on_group_key(bool async) - { - await base.GroupBy_aggregate_projecting_conditional_expression_based_on_group_key(async); - - AssertSql( - """ -SELECT CASE - WHEN o."OrderDate" IS NULL THEN 'is null' - ELSE 'is not null' -END AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."OrderDate" -"""); - } - - #endregion GroupByProperty - - #region GroupByAnonymousAggregate - - public override async Task GroupBy_anonymous_Select_Average(bool async) - { - await base.GroupBy_anonymous_Select_Average(async); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_Select_Count(bool async) - { - await base.GroupBy_anonymous_Select_Count(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_Select_LongCount(bool async) - { - await base.GroupBy_anonymous_Select_LongCount(async); - - AssertSql( - """ -SELECT count(*) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_Select_Max(bool async) - { - await base.GroupBy_anonymous_Select_Max(async); - - AssertSql( - """ -SELECT max(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_Select_Min(bool async) - { - await base.GroupBy_anonymous_Select_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_Select_Sum(bool async) - { - await base.GroupBy_anonymous_Select_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_Select_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_anonymous_Select_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_anonymous_with_alias_Select_Key_Sum(bool async) - { - await base.GroupBy_anonymous_with_alias_Select_Key_Sum(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Composite_Select_Average(bool async) - { - await base.GroupBy_Composite_Select_Average(async); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision) -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Count(bool async) - { - await base.GroupBy_Composite_Select_Count(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_LongCount(bool async) - { - await base.GroupBy_Composite_Select_LongCount(async); - - AssertSql( - """ -SELECT count(*) -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Max(bool async) - { - await base.GroupBy_Composite_Select_Max(async); - - AssertSql( - """ -SELECT max(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Min(bool async) - { - await base.GroupBy_Composite_Select_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Sum(bool async) - { - await base.GroupBy_Composite_Select_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_Composite_Select_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_Average(bool async) - { - await base.GroupBy_Composite_Select_Key_Average(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", avg(o."OrderID"::double precision) AS "Average" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_Count(bool async) - { - await base.GroupBy_Composite_Select_Key_Count(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", count(*)::int AS "Count" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_LongCount(bool async) - { - await base.GroupBy_Composite_Select_Key_LongCount(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", count(*) AS "LongCount" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_Max(bool async) - { - await base.GroupBy_Composite_Select_Key_Max(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", max(o."OrderID") AS "Max" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_Min(bool async) - { - await base.GroupBy_Composite_Select_Key_Min(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", min(o."OrderID") AS "Min" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_Sum(bool async) - { - await base.GroupBy_Composite_Select_Key_Sum(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Key_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_Composite_Select_Key_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT o."CustomerID", o."EmployeeID", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_Composite_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID", o."EmployeeID", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Sum_Min_Key_flattened_Max_Avg(bool async) - { - await base.GroupBy_Composite_Select_Sum_Min_Key_flattened_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID", o."EmployeeID", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Dto_as_key_Select_Sum(bool async) - { - await base.GroupBy_Dto_as_key_Select_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", o."CustomerID", o."EmployeeID" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Dto_as_element_selector_Select_Sum(bool async) - { - await base.GroupBy_Dto_as_element_selector_Select_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."EmployeeID"::bigint), 0.0)::bigint AS "Sum", o."CustomerID" AS "Key" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Composite_Select_Dto_Sum_Min_Key_flattened_Max_Avg(bool async) - { - await base.GroupBy_Composite_Select_Dto_Sum_Min_Key_flattened_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID" AS "CustomerId", o."EmployeeID" AS "EmployeeId", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Composite_Select_Sum_Min_part_Key_flattened_Max_Avg(bool async) - { - await base.GroupBy_Composite_Select_Sum_Min_part_Key_flattened_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", o."CustomerID", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID", o."EmployeeID" -"""); - } - - public override async Task GroupBy_Constant_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_Constant_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_Constant_with_element_selector_Select_Sum(bool async) - { - await base.GroupBy_Constant_with_element_selector_Select_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", 2 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_Constant_with_element_selector_Select_Sum2(bool async) - { - await base.GroupBy_Constant_with_element_selector_Select_Sum2(async); - - AssertSql( - """ -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", 2 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_Constant_with_element_selector_Select_Sum3(bool async) - { - await base.GroupBy_Constant_with_element_selector_Select_Sum3(async); - - AssertSql( - """ -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", 2 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_after_predicate_Constant_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_Constant_with_element_selector_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_Constant_with_element_selector_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", o0."Key" -FROM ( - SELECT o."OrderID", 2 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_constant_with_where_on_grouping_with_aggregate_operators(bool async) - { - await base.GroupBy_constant_with_where_on_grouping_with_aggregate_operators(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."Key" -ORDER BY o0."Key" NULLS FIRST -"""); - } - - public override async Task GroupBy_param_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_param_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -@a='2' - -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 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_param_with_element_selector_Select_Sum(bool async) - { - await base.GroupBy_param_with_element_selector_Select_Sum(async); - - AssertSql( - """ -@a='2' - -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", @a AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_param_with_element_selector_Select_Sum2(bool async) - { - await base.GroupBy_param_with_element_selector_Select_Sum2(async); - - AssertSql( - """ -@a='2' - -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", @a AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_param_with_element_selector_Select_Sum3(bool async) - { - await base.GroupBy_param_with_element_selector_Select_Sum3(async); - - AssertSql( - """ -@a='2' - -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", @a AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_param_with_element_selector_Select_Sum_Min_Key_Max_Avg(bool async) - { - await base.GroupBy_param_with_element_selector_Select_Sum_Min_Key_Max_Avg(async); - - AssertSql( - """ -@a='2' - -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum", o0."Key" -FROM ( - SELECT o."OrderID", @a AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_anonymous_key_type_mismatch_with_aggregate(bool async) - { - await base.GroupBy_anonymous_key_type_mismatch_with_aggregate(async); - - AssertSql( - """ -SELECT count(*)::int AS "I0", o0."I0" AS "I1" -FROM ( - SELECT date_part('year', o."OrderDate")::int AS "I0" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."I0" -ORDER BY o0."I0" NULLS FIRST -"""); - } - - #endregion GroupByAnonymousAggregate - - #region GroupByWithElementSelectorAggregate - - public override async Task GroupBy_Property_scalar_element_selector_Average(bool async) - { - await base.GroupBy_Property_scalar_element_selector_Average(async); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_scalar_element_selector_Count(bool async) - { - await base.GroupBy_Property_scalar_element_selector_Count(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_scalar_element_selector_LongCount(bool async) - { - await base.GroupBy_Property_scalar_element_selector_LongCount(async); - - AssertSql( - """ -SELECT count(*) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_scalar_element_selector_Max(bool async) - { - await base.GroupBy_Property_scalar_element_selector_Max(async); - - AssertSql( - """ -SELECT max(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_scalar_element_selector_Min(bool async) - { - await base.GroupBy_Property_scalar_element_selector_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_scalar_element_selector_Sum(bool async) - { - await base.GroupBy_Property_scalar_element_selector_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_scalar_element_selector_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_Property_scalar_element_selector_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_Average(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_Average(async); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_Count(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_Count(async); - - AssertSql( - """ -SELECT count(*)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_LongCount(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_LongCount(async); - - AssertSql( - """ -SELECT count(*) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_Max(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_Max(async); - - AssertSql( - """ -SELECT max(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_Min(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_Sum(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_anonymous_element_selector_Sum_Min_Max_Avg(bool async) - { - await base.GroupBy_Property_anonymous_element_selector_Sum_Min_Max_Avg(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."EmployeeID") AS "Min", max(o."EmployeeID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_element_selector_complex_aggregate(bool async) - { - await base.GroupBy_element_selector_complex_aggregate(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID" + 1), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_element_selector_complex_aggregate2(bool async) - { - await base.GroupBy_element_selector_complex_aggregate2(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID" + 1), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_element_selector_complex_aggregate3(bool async) - { - await base.GroupBy_element_selector_complex_aggregate3(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID" + 1), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_element_selector_complex_aggregate4(bool async) - { - await base.GroupBy_element_selector_complex_aggregate4(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID" + 1), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task Element_selector_with_case_block_repeated_inside_another_case_block_in_projection(bool async) - { - await base.Element_selector_with_case_block_repeated_inside_another_case_block_in_projection(async); - - AssertSql( - """ -SELECT o."OrderID", COALESCE(sum(CASE - WHEN o."CustomerID" = 'ALFKI' THEN CASE - WHEN o."OrderID" > 1000 THEN o."OrderID" - ELSE -o."OrderID" - END - ELSE -CASE - WHEN o."OrderID" > 1000 THEN o."OrderID" - ELSE -o."OrderID" - END -END), 0)::int AS "Aggregate" -FROM "Orders" AS o -GROUP BY o."OrderID" -"""); - } - - public override async Task GroupBy_conditional_properties(bool async) - { - await base.GroupBy_conditional_properties(async); - - AssertSql( - """ -SELECT o0."OrderMonth", o0."CustomerID" AS "Customer", count(*)::int AS "Count" -FROM ( - SELECT o."CustomerID", NULL AS "OrderMonth" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."OrderMonth", o0."CustomerID" -"""); - } - - #endregion GroupByWithElementSelectorAggregate - - #region GroupByAfterComposition - - public override async Task GroupBy_empty_key_Aggregate(bool async) - { - await base.GroupBy_empty_key_Aggregate(async); - - AssertSql( - """ -SELECT COALESCE(sum(o0."OrderID"), 0)::int -FROM ( - SELECT o."OrderID", 1 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_empty_key_Aggregate_Key(bool async) - { - await base.GroupBy_empty_key_Aggregate_Key(async); - - AssertSql( - """ -SELECT COALESCE(sum(o0."OrderID"), 0)::int AS "Sum" -FROM ( - SELECT o."OrderID", 1 AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task OrderBy_GroupBy_Aggregate(bool async) - { - await base.OrderBy_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task OrderBy_Skip_GroupBy_Aggregate(bool async) - { - await base.OrderBy_Skip_GroupBy_Aggregate(async); - - AssertSql( - """ -@p='80' - -SELECT avg(o0."OrderID"::double precision) -FROM ( - SELECT o."OrderID", o."CustomerID" - FROM "Orders" AS o - ORDER BY o."OrderID" NULLS FIRST - OFFSET @p -) AS o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task OrderBy_Take_GroupBy_Aggregate(bool async) - { - await base.OrderBy_Take_GroupBy_Aggregate(async); - - AssertSql( - """ -@p='500' - -SELECT min(o0."OrderID") -FROM ( - SELECT o."OrderID", o."CustomerID" - FROM "Orders" AS o - ORDER BY o."OrderID" NULLS FIRST - LIMIT @p -) AS o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task OrderBy_Skip_Take_GroupBy_Aggregate(bool async) - { - await base.OrderBy_Skip_Take_GroupBy_Aggregate(async); - - AssertSql( - """ -@p0='500' -@p='80' - -SELECT max(o0."OrderID") -FROM ( - SELECT o."OrderID", o."CustomerID" - FROM "Orders" AS o - ORDER BY o."OrderID" NULLS FIRST - LIMIT @p0 OFFSET @p -) AS o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task Distinct_GroupBy_Aggregate(bool async) - { - await base.Distinct_GroupBy_Aggregate(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task Anonymous_projection_Distinct_GroupBy_Aggregate(bool async) - { - await base.Anonymous_projection_Distinct_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT o0."EmployeeID" AS "Key", count(*)::int AS c -FROM ( - SELECT DISTINCT o."OrderID", o."EmployeeID" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."EmployeeID" -"""); - } - - public override async Task SelectMany_GroupBy_Aggregate(bool async) - { - await base.SelectMany_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT o."EmployeeID" AS "Key", count(*)::int AS c -FROM "Customers" AS c -INNER JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -GROUP BY o."EmployeeID" -"""); - } - - public override async Task Join_GroupBy_Aggregate(bool async) - { - await base.Join_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT c."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Count" -FROM "Orders" AS o -INNER JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY c."CustomerID" -"""); - } - - public override async Task GroupBy_required_navigation_member_Aggregate(bool async) - { - await base.GroupBy_required_navigation_member_Aggregate(async); - - AssertSql( - """ -SELECT o0."CustomerID" AS "CustomerId", count(*)::int AS "Count" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -GROUP BY o0."CustomerID" -"""); - } - - public override async Task Join_complex_GroupBy_Aggregate(bool async) - { - await base.Join_complex_GroupBy_Aggregate(async); - - AssertSql( - """ -@p='100' -@p1='50' -@p0='10' - -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 -) 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 @p1 OFFSET @p0 -) AS c0 ON o0."CustomerID" = c0."CustomerID" -GROUP BY c0."CustomerID" -"""); - } - - public override async Task GroupJoin_GroupBy_Aggregate(bool async) - { - await base.GroupJoin_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Average" -FROM "Customers" AS c -LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -WHERE o."OrderID" IS NOT NULL -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupJoin_GroupBy_Aggregate_2(bool async) - { - await base.GroupJoin_GroupBy_Aggregate_2(async); - - AssertSql( - """ -SELECT c."CustomerID" AS "Key", max(c."City") AS "Max" -FROM "Customers" AS c -LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -GROUP BY c."CustomerID" -"""); - } - - public override async Task GroupJoin_GroupBy_Aggregate_3(bool async) - { - await base.GroupJoin_GroupBy_Aggregate_3(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", avg(o."OrderID"::double precision) AS "Average" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupJoin_GroupBy_Aggregate_4(bool async) - { - await base.GroupJoin_GroupBy_Aggregate_4(async); - - AssertSql( - """ -SELECT c."CustomerID" AS "Value", max(c."City") AS "Max" -FROM "Customers" AS c -LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -GROUP BY c."CustomerID" -"""); - } - - public override async Task GroupJoin_GroupBy_Aggregate_5(bool async) - { - await base.GroupJoin_GroupBy_Aggregate_5(async); - - AssertSql( - """ -SELECT o."OrderID" AS "Value", avg(o."OrderID"::double precision) AS "Average" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY o."OrderID" -"""); - } - - public override async Task GroupBy_optional_navigation_member_Aggregate(bool async) - { - await base.GroupBy_optional_navigation_member_Aggregate(async); - - AssertSql( - """ -SELECT c."Country", count(*)::int AS "Count" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY c."Country" -"""); - } - - public override async Task GroupJoin_complex_GroupBy_Aggregate(bool async) - { - await base.GroupJoin_complex_GroupBy_Aggregate(async); - - AssertSql( - """ -@p0='50' -@p='10' -@p1='100' - -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 @p0 OFFSET @p -) AS c0 -INNER JOIN ( - SELECT o."OrderID", o."CustomerID" - FROM "Orders" AS o - WHERE o."OrderID" < 10400 - ORDER BY o."OrderDate" NULLS FIRST - LIMIT @p1 -) AS o0 ON c0."CustomerID" = o0."CustomerID" -WHERE o0."OrderID" > 10300 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task Self_join_GroupBy_Aggregate(bool async) - { - await base.Self_join_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", avg(o0."OrderID"::double precision) AS "Count" -FROM "Orders" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -WHERE o."OrderID" < 10400 -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_multi_navigation_members_Aggregate(bool async) - { - await base.GroupBy_multi_navigation_members_Aggregate(async); - - AssertSql( - """ -SELECT o0."CustomerID", p."ProductName", count(*)::int AS "Count" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -INNER JOIN "Products" AS p ON o."ProductID" = p."ProductID" -GROUP BY o0."CustomerID", p."ProductName" -"""); - } - - public override async Task Union_simple_groupby(bool async) - { - await base.Union_simple_groupby(async); - - AssertSql( - """ -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 - WHERE c."ContactTitle" = 'Owner' - 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."City" = 'México D.F.' -) AS u -GROUP BY u."City" -"""); - } - - public override async Task Select_anonymous_GroupBy_Aggregate(bool async) - { - await base.Select_anonymous_GroupBy_Aggregate(async); - - AssertSql( - """ -SELECT min(o."OrderDate") AS "Min", max(o."OrderDate") AS "Max", COALESCE(sum(o."OrderID"), 0)::int AS "Sum", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -WHERE o."OrderID" < 10300 -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_principal_key_property_optimization(bool async) - { - await base.GroupBy_principal_key_property_optimization(async); - - AssertSql( - """ -SELECT c."CustomerID" AS "Key", count(*)::int AS "Count" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY c."CustomerID" -"""); - } - - public override async Task GroupBy_after_anonymous_projection_and_distinct_followed_by_another_anonymous_projection(bool async) - { - await base.GroupBy_after_anonymous_projection_and_distinct_followed_by_another_anonymous_projection(async); - - AssertSql( - """ -SELECT o0."CustomerID" AS "Key", count(*)::int AS "Count" -FROM ( - SELECT DISTINCT o."CustomerID", o."OrderID" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task GroupBy_complex_key_aggregate(bool async) - { - await base.GroupBy_complex_key_aggregate(async); - - AssertSql( - """ -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 s -GROUP BY s."Key" -"""); - } - - public override async Task GroupBy_complex_key_aggregate_2(bool async) - { - await base.GroupBy_complex_key_aggregate_2(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."Key" -"""); - } - - public override async Task Select_collection_of_scalar_before_GroupBy_aggregate(bool async) - { - await base.Select_collection_of_scalar_before_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT c."City" AS "Key", count(*)::int AS "Count" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - #endregion GroupByAfterComposition - - #region GroupByAggregateComposition - - public override async Task GroupBy_OrderBy_key(bool async) - { - await base.GroupBy_OrderBy_key(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS c -FROM "Orders" AS o -GROUP BY o."CustomerID" -ORDER BY o."CustomerID" NULLS FIRST -"""); - } - - public override async Task GroupBy_OrderBy_count(bool async) - { - await base.GroupBy_OrderBy_count(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" -FROM "Orders" AS o -GROUP BY o."CustomerID" -ORDER BY count(*)::int NULLS FIRST, o."CustomerID" NULLS FIRST -"""); - } - - public override async Task GroupBy_OrderBy_count_Select_sum(bool async) - { - await base.GroupBy_OrderBy_count_Select_sum(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -ORDER BY count(*)::int NULLS FIRST, o."CustomerID" NULLS FIRST -"""); - } - - public override async Task GroupBy_aggregate_Contains(bool async) - { - await base.GroupBy_aggregate_Contains(async); - - AssertSql( - """ -SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Orders" AS o -WHERE EXISTS ( - SELECT 1 - FROM "Orders" AS o0 - GROUP BY o0."CustomerID" - HAVING count(*)::int > 30 AND (o0."CustomerID" = o."CustomerID" OR (o0."CustomerID" IS NULL AND o."CustomerID" IS NULL))) -"""); - } - - public override async Task GroupBy_aggregate_Pushdown(bool async) - { - await base.GroupBy_aggregate_Pushdown(async); - - AssertSql( - """ -@p='20' -@p0='4' - -SELECT o0."CustomerID" -FROM ( - SELECT o."CustomerID" - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 10 - ORDER BY o."CustomerID" NULLS FIRST - LIMIT @p -) AS o0 -ORDER BY o0."CustomerID" NULLS FIRST -OFFSET @p0 -"""); - } - - public override async Task GroupBy_aggregate_using_grouping_key_Pushdown(bool async) - { - await base.GroupBy_aggregate_using_grouping_key_Pushdown(async); - - AssertSql( - """ -@p='20' -@p0='4' - -SELECT o0."Key", o0."Max" -FROM ( - SELECT o."CustomerID" AS "Key", max(o."CustomerID") AS "Max" - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 10 - ORDER BY o."CustomerID" NULLS FIRST - LIMIT @p -) AS o0 -ORDER BY o0."Key" NULLS FIRST -OFFSET @p0 -"""); - } - - public override async Task GroupBy_aggregate_Pushdown_followed_by_projecting_Length(bool async) - { - await base.GroupBy_aggregate_Pushdown_followed_by_projecting_Length(async); - - AssertSql( - """ -@p='20' -@p0='4' - -SELECT length(o0."CustomerID")::int -FROM ( - SELECT o."CustomerID" - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 10 - ORDER BY o."CustomerID" NULLS FIRST - LIMIT @p -) AS o0 -ORDER BY o0."CustomerID" NULLS FIRST -OFFSET @p0 -"""); - } - - public override async Task GroupBy_aggregate_Pushdown_followed_by_projecting_constant(bool async) - { - await base.GroupBy_aggregate_Pushdown_followed_by_projecting_constant(async); - - AssertSql( - """ -@p='20' -@p0='4' - -SELECT 5 -FROM ( - SELECT o."CustomerID" - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 10 - ORDER BY o."CustomerID" NULLS FIRST - LIMIT @p -) AS o0 -ORDER BY o0."CustomerID" NULLS FIRST -OFFSET @p0 -"""); - } - - public override async Task GroupBy_filter_key(bool async) - { - await base.GroupBy_filter_key(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS c -FROM "Orders" AS o -GROUP BY o."CustomerID" -HAVING o."CustomerID" = 'ALFKI' -"""); - } - - public override async Task GroupBy_filter_count(bool async) - { - await base.GroupBy_filter_count(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" -FROM "Orders" AS o -GROUP BY o."CustomerID" -HAVING count(*)::int > 4 -"""); - } - - public override async Task GroupBy_count_filter(bool async) - { - await base.GroupBy_count_filter(async); - - AssertSql( - """ -SELECT o0."Key" AS "Name", count(*)::int AS "Count" -FROM ( - SELECT 'Order' AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -HAVING count(*)::int > 0 -"""); - } - - public override async Task GroupBy_filter_count_OrderBy_count_Select_sum(bool async) - { - await base.GroupBy_filter_count_OrderBy_count_Select_sum(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS "Count", COALESCE(sum(o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -HAVING count(*)::int > 4 -ORDER BY count(*)::int NULLS FIRST, o."CustomerID" NULLS FIRST -"""); - } - - public override async Task GroupBy_Aggregate_Join(bool async) - { - await base.GroupBy_Aggregate_Join(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", 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 o0 -INNER JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" -INNER JOIN "Orders" AS o1 ON o0."LastOrderID" = o1."OrderID" -"""); - } - - public override async Task GroupBy_Aggregate_Join_converted_from_SelectMany(bool async) - { - await base.GroupBy_Aggregate_Join_converted_from_SelectMany(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" -FROM "Customers" AS c -INNER JOIN ( - SELECT o."CustomerID" - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 5 -) AS o0 ON c."CustomerID" = o0."CustomerID" -"""); - } - - public override async Task GroupBy_Aggregate_LeftJoin_converted_from_SelectMany(bool async) - { - await base.GroupBy_Aggregate_LeftJoin_converted_from_SelectMany(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" -FROM "Customers" AS c -LEFT JOIN ( - SELECT o."CustomerID" - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 5 -) AS o0 ON c."CustomerID" = o0."CustomerID" -"""); - } - - public override async Task Join_GroupBy_Aggregate_multijoins(bool async) - { - await base.Join_GroupBy_Aggregate_multijoins(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", 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 o0 ON c."CustomerID" = o0."CustomerID" -INNER JOIN "Orders" AS o1 ON o0."LastOrderID" = o1."OrderID" -"""); - } - - public override async Task Join_GroupBy_Aggregate_single_join(bool async) - { - await base.Join_GroupBy_Aggregate_single_join(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."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 o0 ON c."CustomerID" = o0."CustomerID" -"""); - } - - public override async Task Join_GroupBy_Aggregate_with_another_join(bool async) - { - await base.Join_GroupBy_Aggregate_with_another_join(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."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 o0 ON c."CustomerID" = o0."CustomerID" -INNER JOIN "Orders" AS o1 ON c."CustomerID" = o1."CustomerID" -"""); - } - - public override async Task Join_GroupBy_Aggregate_distinct_single_join(bool async) - { - await base.Join_GroupBy_Aggregate_distinct_single_join(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", o1."LastOrderID" -FROM "Customers" AS c -INNER JOIN ( - 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 o0 - GROUP BY o0."CustomerID", o0."Year" - HAVING count(*)::int > 5 -) AS o1 ON c."CustomerID" = o1."CustomerID" -"""); - } - - public override async Task Join_GroupBy_Aggregate_with_left_join(bool async) - { - await base.Join_GroupBy_Aggregate_with_left_join(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."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 o0 ON c."CustomerID" = o0."CustomerID" -WHERE c."CustomerID" LIKE 'A%' -"""); - } - - public override async Task Join_GroupBy_Aggregate_in_subquery(bool async) - { - await base.Join_GroupBy_Aggregate_in_subquery(async); - - AssertSql( - """ -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" - FROM "Orders" AS o0 - GROUP BY o0."CustomerID" - HAVING count(*)::int > 5 - ) AS o1 ON c."CustomerID" = o1."CustomerID" -) AS s ON o."CustomerID" = s."CustomerID" -WHERE o."OrderID" < 10400 -"""); - } - - public override async Task Join_GroupBy_Aggregate_on_key(bool async) - { - await base.Join_GroupBy_Aggregate_on_key(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."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 o0 ON c."CustomerID" = o0."Key" -"""); - } - - public override async Task GroupBy_with_result_selector(bool async) - { - await base.GroupBy_with_result_selector(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID"), 0)::int AS "Sum", min(o."OrderID") AS "Min", max(o."OrderID") AS "Max", avg(o."OrderID"::double precision) AS "Avg" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Sum_constant(bool async) - { - await base.GroupBy_Sum_constant(async); - - AssertSql( - """ -SELECT COALESCE(sum(1), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Sum_constant_cast(bool async) - { - await base.GroupBy_Sum_constant_cast(async); - - AssertSql( - """ -SELECT COALESCE(sum(1), 0.0)::bigint -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task Distinct_GroupBy_OrderBy_key(bool async) - { - await base.Distinct_GroupBy_OrderBy_key(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."CustomerID" -ORDER BY o0."CustomerID" NULLS FIRST -"""); - } - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/28410")] - public override async Task Select_nested_collection_with_groupby(bool async) - { - await base.Select_nested_collection_with_groupby(async); - - AssertSql( - """ -SELECT EXISTS ( - SELECT 1 - FROM "Orders" AS o - WHERE c."CustomerID" = o."CustomerID"), c."CustomerID", t."OrderID" -FROM "Customers" AS c -LEFT JOIN LATERAL ( - SELECT o0."OrderID" - FROM "Orders" AS o0 - WHERE c."CustomerID" = o0."CustomerID" - GROUP BY o0."OrderID" -) AS t ON TRUE -WHERE c."CustomerID" LIKE 'F%' -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Select_uncorrelated_collection_with_groupby_works(bool async) - { - await base.Select_uncorrelated_collection_with_groupby_works(async); - - AssertSql( - """ -SELECT c."CustomerID", o0."OrderID" -FROM "Customers" AS c -LEFT JOIN LATERAL ( - SELECT o."OrderID" - FROM "Orders" AS o - GROUP BY o."OrderID" -) AS o0 ON TRUE -WHERE c."CustomerID" LIKE 'A%' -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Select_uncorrelated_collection_with_groupby_multiple_collections_work(bool async) - { - await base.Select_uncorrelated_collection_with_groupby_multiple_collections_work(async); - - AssertSql( - """ -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 p1 ON TRUE -LEFT JOIN LATERAL ( - SELECT count(*)::int AS c, p0."ProductID" - FROM "Products" AS p0 - GROUP BY p0."ProductID" -) AS p2 ON TRUE -WHERE o."CustomerID" LIKE 'A%' -ORDER BY o."OrderID" NULLS FIRST, p1."ProductID" NULLS FIRST -"""); - } - - public override async Task Select_GroupBy_All(bool async) - { - await base.Select_GroupBy_All(async); - - AssertSql( - """ -SELECT NOT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING o."CustomerID" <> 'ALFKI' OR o."CustomerID" IS NULL) -"""); - } - - #endregion GroupByAggregateComposition - - #region GroupByAggregateChainComposition - - public override async Task GroupBy_Where_Average(bool async) - { - await base.GroupBy_Where_Average(async); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision) FILTER (WHERE o."OrderID" < 10300) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Count(bool async) - { - await base.GroupBy_Where_Count(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_LongCount(bool async) - { - await base.GroupBy_Where_LongCount(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Max(bool async) - { - await base.GroupBy_Where_Max(async); - - AssertSql( - """ -SELECT max(o."OrderID") FILTER (WHERE o."OrderID" < 10300) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Min(bool async) - { - await base.GroupBy_Where_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") FILTER (WHERE o."OrderID" < 10300) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Sum(bool async) - { - await base.GroupBy_Where_Sum(async); - - AssertSql( - """ -SELECT COALESCE(sum(o."OrderID") FILTER (WHERE o."OrderID" < 10300), 0)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Count_with_predicate(bool async) - { - await base.GroupBy_Where_Count_with_predicate(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Where_Count(bool async) - { - await base.GroupBy_Where_Where_Count(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Select_Where_Count(bool async) - { - await base.GroupBy_Where_Select_Where_Count(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Where_Select_Where_Select_Min(bool async) - { - await base.GroupBy_Where_Select_Where_Select_Min(async); - - AssertSql( - """ -SELECT min(o."OrderID") FILTER (WHERE o."OrderID" < 10300 AND o."OrderDate" IS NOT NULL AND date_part('year', o."OrderDate")::int = 1997) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_multiple_Count_with_predicate(bool async) - { - await base.GroupBy_multiple_Count_with_predicate(async); - - AssertSql( - """ -SELECT o."CustomerID", count(*)::int AS "All", count(*) FILTER (WHERE o."OrderID" < 11000)::int AS "TenK", count(*) FILTER (WHERE o."OrderID" < 12000)::int AS "EleventK" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_multiple_Sum_with_conditional_projection(bool async) - { - await base.GroupBy_multiple_Sum_with_conditional_projection(async); - - AssertSql( - """ -SELECT o."CustomerID", COALESCE(sum(CASE - WHEN o."OrderID" < 11000 THEN o."OrderID" - ELSE 0 -END), 0)::int AS "TenK", COALESCE(sum(CASE - WHEN o."OrderID" >= 11000 THEN o."OrderID" - ELSE 0 -END), 0)::int AS "EleventK" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_multiple_Sum_with_Select_conditional_projection(bool async) - { - await base.GroupBy_multiple_Sum_with_Select_conditional_projection(async); - - AssertSql( - """ -SELECT o."CustomerID", COALESCE(sum(CASE - WHEN o."OrderID" < 11000 THEN o."OrderID" - ELSE 0 -END), 0)::int AS "TenK", COALESCE(sum(CASE - WHEN o."OrderID" >= 11000 THEN o."OrderID" - ELSE 0 -END), 0)::int AS "EleventK" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Key_as_part_of_element_selector(bool async) - { - await base.GroupBy_Key_as_part_of_element_selector(async); - - AssertSql( - """ -SELECT o."OrderID" AS "Key", avg(o."OrderID"::double precision) AS "Avg", max(o."OrderDate") AS "Max" -FROM "Orders" AS o -GROUP BY o."OrderID" -"""); - } - - public override async Task GroupBy_composite_Key_as_part_of_element_selector(bool async) - { - await base.GroupBy_composite_Key_as_part_of_element_selector(async); - - AssertSql( - """ -SELECT o."OrderID", o."CustomerID", avg(o."OrderID"::double precision) AS "Avg", max(o."OrderDate") AS "Max" -FROM "Orders" AS o -GROUP BY o."OrderID", o."CustomerID" -"""); - } - - public override async Task GroupBy_with_aggregate_through_navigation_property(bool async) - { - await base.GroupBy_with_aggregate_through_navigation_property(async); - - AssertSql( - """ -SELECT ( - SELECT max(c."Region") - FROM "Orders" AS o0 - LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" - WHERE o."EmployeeID" = o0."EmployeeID" OR (o."EmployeeID" IS NULL AND o0."EmployeeID" IS NULL)) AS max -FROM "Orders" AS o -GROUP BY o."EmployeeID" -"""); - } - - public override async Task GroupBy_with_aggregate_containing_complex_where(bool async) - { - await base.GroupBy_with_aggregate_containing_complex_where(async); - - AssertSql( - """ -SELECT o."EmployeeID" AS "Key", ( - SELECT max(o0."OrderID") - FROM "Orders" AS o0 - WHERE o0."EmployeeID"::bigint = CAST(max(o."OrderID") * 6 AS bigint) OR (o0."EmployeeID" IS NULL AND max(o."OrderID") IS NULL)) AS "Max" -FROM "Orders" AS o -GROUP BY o."EmployeeID" -"""); - } - - #endregion GroupByWithoutAggregate - - #region GroupBySelectFirst - - public override async Task GroupBy_Shadow(bool async) - { - await base.GroupBy_Shadow(async); - - AssertSql( - """ -SELECT ( - SELECT e0."Title" - FROM "Employees" AS e0 - WHERE e0."Title" = 'Sales Representative' AND e0."EmployeeID" = 1 AND (e."Title" = e0."Title" OR (e."Title" IS NULL AND e0."Title" IS NULL)) - LIMIT 1) -FROM "Employees" AS e -WHERE e."Title" = 'Sales Representative' AND e."EmployeeID" = 1 -GROUP BY e."Title" -"""); - } - - public override async Task GroupBy_Shadow2(bool async) - { - await base.GroupBy_Shadow2(async); - - AssertSql( - """ -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 e1 -LEFT JOIN ( - 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 e2 - WHERE e2.row <= 1 -) AS e3 ON e1."Title" = e3."Title" -"""); - } - - public override async Task GroupBy_Shadow3(bool async) - { - await base.GroupBy_Shadow3(async); - - AssertSql( - """ -SELECT ( - SELECT e0."Title" - FROM "Employees" AS e0 - WHERE e0."EmployeeID" = 1 AND e."EmployeeID" = e0."EmployeeID" - LIMIT 1) -FROM "Employees" AS e -WHERE e."EmployeeID" = 1 -GROUP BY e."EmployeeID" -"""); - } - - public override async Task GroupBy_select_grouping_list(bool async) - { - await base.GroupBy_select_grouping_list(async); - - AssertSql( - """ -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 c1 -LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" -ORDER BY c1."City" NULLS FIRST -"""); - } - - public override async Task GroupBy_select_grouping_array(bool async) - { - await base.GroupBy_select_grouping_array(async); - - AssertSql( - """ -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 c1 -LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" -ORDER BY c1."City" NULLS FIRST -"""); - } - - public override async Task GroupBy_select_grouping_composed_list(bool async) - { - await base.GroupBy_select_grouping_composed_list(async); - - AssertSql( - """ -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 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 c2 ON c1."City" = c2."City" -ORDER BY c1."City" NULLS FIRST -"""); - } - - public override async Task GroupBy_select_grouping_composed_list_2(bool async) - { - await base.GroupBy_select_grouping_composed_list_2(async); - - AssertSql( - """ -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 c1 -LEFT JOIN "Customers" AS c0 ON c1."City" = c0."City" -ORDER BY c1."City" NULLS FIRST, c0."CustomerID" NULLS FIRST -"""); - } - - public override async Task Select_GroupBy_SelectMany(bool async) - { - await base.Select_GroupBy_SelectMany(async); - - AssertSql(); - } - - #endregion GroupByEntityType - - #region ResultOperatorsAfterGroupBy - - public override async Task Count_after_GroupBy_aggregate(bool async) - { - await base.Count_after_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT count(*)::int -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -"""); - } - - public override async Task LongCount_after_GroupBy_aggregate(bool async) - { - await base.LongCount_after_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT count(*) -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -"""); - } - - public override async Task GroupBy_Select_Distinct_aggregate(bool async) - { - await base.GroupBy_Select_Distinct_aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", avg(DISTINCT o."OrderID"::double precision) AS "Average", count(DISTINCT o."EmployeeID")::int AS "Count", count(DISTINCT o."EmployeeID") AS "LongCount", max(o."OrderDate") AS "Max", min(o."OrderDate") AS "Min", COALESCE(sum(DISTINCT o."OrderID"), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_group_Distinct_Select_Distinct_aggregate(bool async) - { - await base.GroupBy_group_Distinct_Select_Distinct_aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", max(o."OrderDate") AS "Max" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_group_Where_Select_Distinct_aggregate(bool async) - { - await base.GroupBy_group_Where_Select_Distinct_aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", max(o."OrderDate") FILTER (WHERE o."OrderDate" IS NOT NULL) AS "Max" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task MinMax_after_GroupBy_aggregate(bool async) - { - await base.MinMax_after_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT min(o0.c) -FROM ( - SELECT COALESCE(sum(o."OrderID"), 0)::int AS c - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -""", - // - """ -SELECT max(o0.c) -FROM ( - SELECT COALESCE(sum(o."OrderID"), 0)::int AS c - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -"""); - } - - public override async Task All_after_GroupBy_aggregate(bool async) - { - await base.All_after_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT NOT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING FALSE) -"""); - } - - public override async Task All_after_GroupBy_aggregate2(bool async) - { - await base.All_after_GroupBy_aggregate2(async); - - AssertSql( - """ -SELECT NOT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING COALESCE(sum(o."OrderID"), 0)::int < 0) -"""); - } - - public override async Task Any_after_GroupBy_aggregate(bool async) - { - await base.Any_after_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID") -"""); - } - - public override async Task Count_after_GroupBy_without_aggregate(bool async) - { - await base.Count_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT count(*)::int -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -"""); - } - - public override async Task Count_with_predicate_after_GroupBy_without_aggregate(bool async) - { - await base.Count_with_predicate_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT count(*)::int -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 1 -) AS o0 -"""); - } - - public override async Task LongCount_after_GroupBy_without_aggregate(bool async) - { - await base.LongCount_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT count(*) -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -"""); - } - - public override async Task LongCount_with_predicate_after_GroupBy_without_aggregate(bool async) - { - await base.LongCount_with_predicate_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT count(*) -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 1 -) AS o0 -"""); - } - - public override async Task Any_after_GroupBy_without_aggregate(bool async) - { - await base.Any_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID") -"""); - } - - public override async Task Any_with_predicate_after_GroupBy_without_aggregate(bool async) - { - await base.Any_with_predicate_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int > 1) -"""); - } - - public override async Task All_with_predicate_after_GroupBy_without_aggregate(bool async) - { - await base.All_with_predicate_after_GroupBy_without_aggregate(async); - - AssertSql( - """ -SELECT NOT EXISTS ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" - HAVING count(*)::int <= 1) -"""); - } - - public override async Task GroupBy_aggregate_followed_by_another_GroupBy_aggregate(bool async) - { - await base.GroupBy_aggregate_followed_by_another_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT o1."Key0" AS "Key", COALESCE(sum(o1."Count"), 0)::int AS "Count" -FROM ( - SELECT o0."Count", 1 AS "Key0" - FROM ( - SELECT count(*)::int AS "Count" - FROM "Orders" AS o - GROUP BY o."CustomerID" - ) AS o0 -) AS o1 -GROUP BY o1."Key0" -"""); - } - - public override async Task GroupBy_Count_in_projection(bool async) - { - await base.GroupBy_Count_in_projection(async); - - AssertSql( - """ -SELECT o."OrderID", o."OrderDate", EXISTS ( - SELECT 1 - FROM "Order Details" AS o0 - 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 -"""); - } - - public override async Task GroupBy_nominal_type_count(bool async) - { - await base.GroupBy_nominal_type_count(async); - - AssertSql( - """ -SELECT count(*)::int -FROM ( - SELECT 1 - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -"""); - } - - public override async Task GroupBy_based_on_renamed_property_simple(bool async) - { - await base.GroupBy_based_on_renamed_property_simple(async); - - AssertSql( - """ -SELECT c."City" AS "Renamed", count(*)::int AS "Count" -FROM "Customers" AS c -GROUP BY c."City" -"""); - } - - public override async Task GroupBy_based_on_renamed_property_complex(bool async) - { - await base.GroupBy_based_on_renamed_property_complex(async); - - AssertSql( - """ -SELECT c0."Renamed" AS "Key", count(*)::int AS "Count" -FROM ( - SELECT DISTINCT c."City" AS "Renamed", c."CustomerID" - FROM "Customers" AS c -) AS c0 -GROUP BY c0."Renamed" -"""); - } - - public override async Task Join_groupby_anonymous_orderby_anonymous_projection(bool async) - { - await base.Join_groupby_anonymous_orderby_anonymous_projection(async); - - AssertSql( - """ -SELECT c."CustomerID", o."OrderDate" -FROM "Customers" AS c -INNER JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -GROUP BY c."CustomerID", o."OrderDate" -ORDER BY o."OrderDate" NULLS FIRST -"""); - } - - public override async Task Odata_groupby_empty_key(bool async) - { - await base.Odata_groupby_empty_key(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_with_group_key_access_thru_navigation(bool async) - { - await base.GroupBy_with_group_key_access_thru_navigation(async); - - AssertSql( - """ -SELECT o0."CustomerID" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -GROUP BY o0."CustomerID" -"""); - } - - public override async Task GroupBy_with_group_key_access_thru_nested_navigation(bool async) - { - await base.GroupBy_with_group_key_access_thru_nested_navigation(async); - - AssertSql( - """ -SELECT c."Country" AS "Key", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" -GROUP BY c."Country" -"""); - } - - #endregion GroupBySelectFirst - - #region GroupByEntityType - - public override async Task GroupBy_with_group_key_being_navigation(bool async) - { - await base.GroupBy_with_group_key_being_navigation(async); - - AssertSql( - """ -SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -GROUP BY o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" -"""); - } - - public override async Task GroupBy_with_group_key_being_nested_navigation(bool async) - { - await base.GroupBy_with_group_key_being_nested_navigation(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", COALESCE(sum(o."OrderID"), 0)::int AS "Aggregate" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" -GROUP BY c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" -"""); - } - - public override async Task GroupBy_with_group_key_being_navigation_with_entity_key_projection(bool async) - { - await base.GroupBy_with_group_key_being_navigation_with_entity_key_projection(async); - - AssertSql( - """ -SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" -FROM "Order Details" AS o -INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" -GROUP BY o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" -"""); - } - - public override async Task GroupBy_with_group_key_being_navigation_with_complex_projection(bool async) - { - await base.GroupBy_with_group_key_being_navigation_with_complex_projection(async); - - AssertSql(); - } - - public override async Task GroupBy_with_order_by_skip_and_another_order_by(bool async) - { - await base.GroupBy_with_order_by_skip_and_another_order_by(async); - - AssertSql( - """ -@p='80' - -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 -) AS o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_Count_with_predicate(bool async) - { - await base.GroupBy_Property_Select_Count_with_predicate(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300)::int -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_Property_Select_LongCount_with_predicate(bool async) - { - await base.GroupBy_Property_Select_LongCount_with_predicate(async); - - AssertSql( - """ -SELECT count(*) FILTER (WHERE o."OrderID" < 10300) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_orderby_projection_with_coalesce_operation(bool async) - { - await base.GroupBy_orderby_projection_with_coalesce_operation(async); - - AssertSql( - """ -SELECT COALESCE(c."City", 'Unknown') AS "Locality", count(*)::int AS "Count" -FROM "Customers" AS c -GROUP BY c."City" -ORDER BY count(*)::int DESC NULLS LAST, c."City" NULLS FIRST -"""); - } - - public override async Task GroupBy_let_orderby_projection_with_coalesce_operation(bool async) - { - await base.GroupBy_let_orderby_projection_with_coalesce_operation(async); - - AssertSql(); - } - - public override async Task GroupBy_Min_Where_optional_relationship(bool async) - { - await base.GroupBy_Min_Where_optional_relationship(async); - - AssertSql( - """ -SELECT c."CustomerID" AS "Key", count(*)::int AS "Count" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY c."CustomerID" -HAVING count(*)::int <> 2 -"""); - } - - public override async Task GroupBy_Min_Where_optional_relationship_2(bool async) - { - await base.GroupBy_Min_Where_optional_relationship_2(async); - - AssertSql( - """ -SELECT c."CustomerID" AS "Key", count(*)::int AS "Count" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY c."CustomerID" -HAVING count(*)::int < 2 OR count(*)::int > 2 -"""); - } - - public override async Task GroupBy_aggregate_over_a_subquery(bool async) - { - await base.GroupBy_aggregate_over_a_subquery(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", ( - SELECT count(*)::int - FROM "Customers" AS c - WHERE c."CustomerID" = o."CustomerID") AS "Count" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_aggregate_join_with_grouping_key(bool async) - { - await base.GroupBy_aggregate_join_with_grouping_key(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."Count" -FROM ( - SELECT o."CustomerID" AS "Key", count(*)::int AS "Count" - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o0 -INNER JOIN "Customers" AS c ON o0."Key" = c."CustomerID" -"""); - } - - public override async Task GroupBy_aggregate_join_with_group_result(bool async) - { - await base.GroupBy_aggregate_join_with_group_result(async); - - AssertSql( - """ -SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" -FROM ( - SELECT o."CustomerID" AS "Key", max(o."OrderDate") AS "LastOrderDate" - FROM "Orders" AS o - GROUP BY o."CustomerID" -) 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)) -"""); - } - - public override async Task GroupBy_aggregate_from_right_side_of_join(bool async) - { - await base.GroupBy_aggregate_from_right_side_of_join(async); - - AssertSql( - """ -@p='10' - -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 o0 ON c."CustomerID" = o0."Key" -ORDER BY o0."Max" NULLS FIRST, c."CustomerID" NULLS FIRST -LIMIT @p OFFSET @p -"""); - } - - public override async Task GroupBy_aggregate_join_another_GroupBy_aggregate(bool async) - { - await base.GroupBy_aggregate_join_another_GroupBy_aggregate(async); - - AssertSql( - """ -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 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 o2 ON o1."Key" = o2."Key" -"""); - } - - public override async Task GroupBy_aggregate_after_skip_0_take_0(bool async) - { - await base.GroupBy_aggregate_after_skip_0_take_0(async); - - AssertSql( - """ -@p='0' - -SELECT o0."CustomerID" AS "Key", count(*)::int AS "Total" -FROM ( - SELECT o."CustomerID" - FROM "Orders" AS o - LIMIT @p OFFSET @p -) AS o0 -GROUP BY o0."CustomerID" -"""); - } - - public override async Task GroupBy_skip_0_take_0_aggregate(bool async) - { - await base.GroupBy_skip_0_take_0_aggregate(async); - - AssertSql( - """ -@p='0' - -SELECT o."CustomerID" AS "Key", count(*)::int AS "Total" -FROM "Orders" AS o -WHERE o."OrderID" > 10500 -GROUP BY o."CustomerID" -LIMIT @p OFFSET @p -"""); - } - - public override async Task GroupBy_aggregate_followed_another_GroupBy_aggregate(bool async) - { - await base.GroupBy_aggregate_followed_another_GroupBy_aggregate(async); - - AssertSql( - """ -SELECT o1."CustomerID" AS "Key", count(*)::int AS "Count" -FROM ( - SELECT o0."CustomerID" - FROM ( - SELECT o."CustomerID", date_part('year', o."OrderDate")::int AS "Year" - FROM "Orders" AS o - ) AS o0 - GROUP BY o0."CustomerID", o0."Year" -) AS o1 -GROUP BY o1."CustomerID" -"""); - } - - public override async Task GroupBy_aggregate_without_selectMany_selecting_first(bool async) - { - await base.GroupBy_aggregate_without_selectMany_selecting_first(async); - - AssertSql( - """ -SELECT o0."OrderID", o0."CustomerID", o0."EmployeeID", o0."OrderDate" -FROM ( - SELECT min(o."OrderID") AS c - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o1 -CROSS JOIN "Orders" AS o0 -WHERE o0."OrderID" = o1.c -"""); - } - - public override async Task GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(bool async) - { - await base.GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(async); - - AssertSql( - """ -SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] -FROM ( - SELECT MIN([o].[OrderID]) AS [c] - FROM [Orders] AS [o] - GROUP BY [o].[CustomerID] -) AS [t] -CROSS JOIN [Orders] AS [o0] -WHERE [o0].[OrderID] = [t].[c] -"""); - } - - public override async Task GroupBy_selecting_grouping_key_list(bool async) - { - await base.GroupBy_selecting_grouping_key_list(async); - - AssertSql( - """ -SELECT o1."CustomerID", o0."CustomerID", o0."OrderID" -FROM ( - SELECT o."CustomerID" - FROM "Orders" AS o - GROUP BY o."CustomerID" -) AS o1 -LEFT JOIN "Orders" AS o0 ON o1."CustomerID" = o0."CustomerID" -ORDER BY o1."CustomerID" NULLS FIRST -"""); - } - - public override async Task GroupBy_with_grouping_key_using_Like(bool async) - { - await base.GroupBy_with_grouping_key_using_Like(async); - - AssertSql( - """ -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 o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_with_grouping_key_DateTime_Day(bool async) - { - await base.GroupBy_with_grouping_key_DateTime_Day(async); - - AssertSql( - """ -SELECT o0."Key", count(*)::int AS "Count" -FROM ( - SELECT date_part('day', o."OrderDate")::int AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task GroupBy_with_cast_inside_grouping_aggregate(bool async) - { - await base.GroupBy_with_cast_inside_grouping_aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", count(*)::int AS "Count", COALESCE(sum(o."OrderID"::bigint), 0.0)::bigint AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - #endregion ResultOperatorsAfterGroupBy - - #region GroupByInSubquery - - public override async Task Complex_query_with_groupBy_in_subquery1(bool async) - { - await base.Complex_query_with_groupBy_in_subquery1(async); - - AssertSql( - """ -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 o0 ON TRUE -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Complex_query_with_groupBy_in_subquery2(bool async) - { - await base.Complex_query_with_groupBy_in_subquery2(async); - - AssertSql( - """ -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 o0 ON TRUE -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Complex_query_with_groupBy_in_subquery3(bool async) - { - await base.Complex_query_with_groupBy_in_subquery3(async); - - AssertSql( - """ -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 o0 ON TRUE -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Group_by_with_projection_into_DTO(bool async) - { - await base.Group_by_with_projection_into_DTO(async); - - AssertSql( - """ -SELECT o."OrderID"::bigint AS "Id", count(*)::int AS "Count" -FROM "Orders" AS o -GROUP BY o."OrderID" -"""); - } - - public override async Task Where_select_function_groupby_followed_by_another_select_with_aggregates(bool async) - { - await base.Where_select_function_groupby_followed_by_another_select_with_aggregates(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(sum(CASE - WHEN 2020 - date_part('year', o."OrderDate")::int <= 30 THEN o."OrderID" - ELSE 0 -END), 0)::int AS "Sum1", COALESCE(sum(CASE - WHEN 2020 - date_part('year', o."OrderDate")::int > 30 AND 2020 - date_part('year', o."OrderDate")::int <= 60 THEN o."OrderID" - ELSE 0 -END), 0)::int AS "Sum2" -FROM "Orders" AS o -WHERE o."CustomerID" LIKE 'A%' -GROUP BY o."CustomerID" -"""); - } - - public override async Task Group_by_column_project_constant(bool async) - { - await base.Group_by_column_project_constant(async); - - AssertSql( - """ -SELECT 42 -FROM "Orders" AS o -GROUP BY o."CustomerID" -ORDER BY o."CustomerID" NULLS FIRST -"""); - } - - public override async Task Key_plus_key_in_projection(bool async) - { - await base.Key_plus_key_in_projection(async); - - AssertSql( - """ -SELECT o."OrderID" + o."OrderID" AS "Value", avg(o."OrderID"::double precision) AS "Average" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -GROUP BY o."OrderID" -"""); - } - - public override async Task Group_by_with_arithmetic_operation_inside_aggregate(bool async) - { - await base.Group_by_with_arithmetic_operation_inside_aggregate(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(sum(o."OrderID" + length(o."CustomerID")::int), 0)::int AS "Sum" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_scalar_subquery(bool async) - { - await base.GroupBy_scalar_subquery(async); - - AssertSql( - """ -SELECT o0."Key", count(*)::int AS "Count" -FROM ( - SELECT ( - SELECT c."ContactName" - FROM "Customers" AS c - WHERE c."CustomerID" = o."CustomerID" - LIMIT 1) AS "Key" - FROM "Orders" AS o -) AS o0 -GROUP BY o0."Key" -"""); - } - - public override async Task AsEnumerable_in_subquery_for_GroupBy(bool async) - { - await base.AsEnumerable_in_subquery_for_GroupBy(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", s."OrderID", s."CustomerID", s."EmployeeID", s."OrderDate", s."CustomerID0" -FROM "Customers" AS c -LEFT JOIN LATERAL ( - 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 o1 - LEFT JOIN ( - 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 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, s."CustomerID0" NULLS FIRST -"""); - } - - public override async Task GroupBy_aggregate_from_multiple_query_in_same_projection(bool async) - { - await base.GroupBy_aggregate_from_multiple_query_in_same_projection(async); - - AssertSql( - """ -SELECT [t].[CustomerID], [t0].[Key], [t0].[C], [t0].[c0] -FROM ( - SELECT [o].[CustomerID] - FROM [Orders] AS [o] - GROUP BY [o].[CustomerID] -) AS [t] -OUTER APPLY ( - SELECT TOP(1) [e].[City] AS [Key], COUNT(*) + ( - SELECT COUNT(*) - FROM [Orders] AS [o0] - WHERE [t].[CustomerID] = [o0].[CustomerID] OR ([t].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) AS [C], 1 AS [c0] - FROM [Employees] AS [e] - WHERE [e].[City] = N'Seattle' - GROUP BY [e].[City] - ORDER BY (SELECT 1) -) AS [t0] -"""); - } - - public override async Task GroupBy_aggregate_from_multiple_query_in_same_projection_2(bool async) - { - await base.GroupBy_aggregate_from_multiple_query_in_same_projection_2(async); - - AssertSql( - """ -SELECT o."CustomerID" AS "Key", COALESCE(( - SELECT count(*)::int + min(o."OrderID") - FROM "Employees" AS e - WHERE e."City" = 'Seattle' - GROUP BY e."City" - ORDER BY (SELECT 1) NULLS FIRST - LIMIT 1), 0) AS "A" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - public override async Task GroupBy_aggregate_from_multiple_query_in_same_projection_3(bool async) - { - await base.GroupBy_aggregate_from_multiple_query_in_same_projection_3(async); - - AssertSql( - """ -SELECT [o].[CustomerID] AS [Key], COALESCE(( - SELECT TOP(1) COUNT(*) + ( - SELECT COUNT(*) - FROM [Orders] AS [o0] - WHERE [o].[CustomerID] = [o0].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) - FROM [Employees] AS [e] - WHERE [e].[City] = N'Seattle' - GROUP BY [e].[City] - ORDER BY COUNT(*) + ( - SELECT COUNT(*) - FROM [Orders] AS [o0] - WHERE [o].[CustomerID] = [o0].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL))), 0) AS [A] -FROM [Orders] AS [o] -GROUP BY [o].[CustomerID] -"""); - } - - public override async Task GroupBy_scalar_aggregate_in_set_operation(bool async) - { - await base.GroupBy_scalar_aggregate_in_set_operation(async); - - AssertSql( - """ -SELECT c."CustomerID", 0 AS "Sequence" -FROM "Customers" AS c -WHERE c."CustomerID" LIKE 'F%' -UNION -SELECT o."CustomerID", 1 AS "Sequence" -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - #endregion GroupByInSubquery - - #region GroupByAndDistinctWithCorrelatedCollection - - public override async Task Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(bool async) - { - await base.Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(async); - - AssertSql( - """ -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 s -LEFT JOIN LATERAL ( - SELECT p."ProductID" - FROM "Products" AS p - GROUP BY p."ProductID" -) AS p1 ON TRUE -LEFT JOIN LATERAL ( - SELECT count(*)::int AS c, p0."ProductID" - FROM "Products" AS p0 - GROUP BY p0."ProductID" -) AS p2 ON TRUE -ORDER BY s."City" NULLS FIRST, p1."ProductID" NULLS FIRST -"""); - } - - public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(bool async) - { - await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(async); - - AssertSql( - """ -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 c0 -LEFT JOIN "Orders" AS o ON c0."CustomerID" = o."CustomerID" -ORDER BY c0."CustomerID" NULLS FIRST -"""); - } - - public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(bool async) - { - await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(async); - - AssertSql( - """ -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 o1 -LEFT JOIN "Orders" AS o0 ON o1."CustomerID" = o0."CustomerID" -ORDER BY o1."CustomerID" NULLS FIRST -"""); - } - - public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) - => await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(async); - - //AssertSql(" "); - public override async Task Complex_query_with_group_by_in_subquery5(bool async) - { - await base.Complex_query_with_group_by_in_subquery5(async); - - AssertSql( - """ -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 - INNER JOIN "Orders" AS o0 ON o."OrderID" = o0."OrderID" - LEFT JOIN "Customers" AS c ON o0."CustomerID" = c."CustomerID" - WHERE c."CustomerID" = 'ALFKI' - GROUP BY o."ProductID" -) AS s -LEFT JOIN LATERAL ( - SELECT c0."CustomerID", c0."City" - FROM "Customers" AS c0 - WHERE length(c0."CustomerID")::int < s.c0 -) AS c1 ON TRUE -ORDER BY s."ProductID" NULLS FIRST, c1."CustomerID" NULLS FIRST -"""); - } - - public override async Task Complex_query_with_groupBy_in_subquery4(bool async) - { - await base.Complex_query_with_groupBy_in_subquery4(async); - - AssertSql( - """ -SELECT c."CustomerID", s1."Sum", s1."Count", s1."Key" -FROM "Customers" AS c -LEFT JOIN LATERAL ( - SELECT COALESCE(sum(s."OrderID"), 0)::int AS "Sum", ( - SELECT count(*)::int - FROM ( - 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 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(c0."City", '') || COALESCE(o."CustomerID", '') AS "Key" - FROM "Orders" AS o - LEFT JOIN "Customers" AS c0 ON o."CustomerID" = c0."CustomerID" - WHERE c."CustomerID" = o."CustomerID" - ) AS s - GROUP BY s."Key" -) AS s1 ON TRUE -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - public override async Task GroupBy_aggregate_SelectMany(bool async) - { - await base.GroupBy_aggregate_SelectMany(async); - - AssertSql(); - } - - public override async Task Final_GroupBy_property_entity(bool async) - { - await base.Final_GroupBy_property_entity(async); - - AssertSql( - """ -SELECT c."City", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" -FROM "Customers" AS c -ORDER BY c."City" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_entity(bool async) - { - await base.Final_GroupBy_entity(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", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Orders" AS o -LEFT JOIN "Customers" AS c ON o."CustomerID" = c."CustomerID" -WHERE o."OrderID" < 10500 -ORDER BY c."CustomerID" NULLS FIRST, c."Address" NULLS FIRST, c."City" NULLS FIRST, c."CompanyName" NULLS FIRST, c."ContactName" NULLS FIRST, c."ContactTitle" NULLS FIRST, c."Country" NULLS FIRST, c."Fax" NULLS FIRST, c."Phone" NULLS FIRST, c."PostalCode" NULLS FIRST, c."Region" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_entity_non_nullable(bool async) - { - await base.Final_GroupBy_property_entity_non_nullable(async); - - AssertSql( - """ -SELECT o."OrderID", o."ProductID", o."Discount", o."Quantity", o."UnitPrice" -FROM "Order Details" AS o -WHERE o."OrderID" < 10500 -ORDER BY o."OrderID" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_anonymous_type(bool async) - { - await base.Final_GroupBy_property_anonymous_type(async); - - AssertSql( - """ -SELECT c."City", c."ContactName", c."ContactTitle" -FROM "Customers" AS c -ORDER BY c."City" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_multiple_properties_entity(bool async) - { - await base.Final_GroupBy_multiple_properties_entity(async); - - AssertSql( - """ -SELECT c."City", c."Region", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode" -FROM "Customers" AS c -ORDER BY c."City" NULLS FIRST, c."Region" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_complex_key_entity(bool async) - { - await base.Final_GroupBy_complex_key_entity(async); - - AssertSql( - """ -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 c0 -ORDER BY c0."City" NULLS FIRST, c0."Region" NULLS FIRST, c0."Constant" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_nominal_type_entity(bool async) - { - await base.Final_GroupBy_nominal_type_entity(async); - - AssertSql( - """ -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 c0 -ORDER BY c0."City" NULLS FIRST, c0."Constant" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_anonymous_type_element_selector(bool async) - { - await base.Final_GroupBy_property_anonymous_type_element_selector(async); - - AssertSql( - """ -SELECT c."City", c."ContactName", c."ContactTitle" -FROM "Customers" AS c -ORDER BY c."City" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_entity_Include_collection(bool async) - { - await base.Final_GroupBy_property_entity_Include_collection(async); - - AssertSql( - """ -SELECT c."City", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Customers" AS c -LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -WHERE c."Country" = 'USA' -ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_entity_projecting_collection(bool async) - { - await base.Final_GroupBy_property_entity_projecting_collection(async); - - AssertSql( - """ -SELECT c."City", c."CustomerID", o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Customers" AS c -LEFT JOIN "Orders" AS o ON c."CustomerID" = o."CustomerID" -WHERE c."Country" = 'USA' -ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_entity_projecting_collection_composed(bool async) - { - await base.Final_GroupBy_property_entity_projecting_collection_composed(async); - - AssertSql( - """ -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 o0 ON c."CustomerID" = o0."CustomerID" -WHERE c."Country" = 'USA' -ORDER BY c."City" NULLS FIRST, c."CustomerID" NULLS FIRST -"""); - } - - public override async Task Final_GroupBy_property_entity_projecting_collection_and_single_result(bool async) - { - await base.Final_GroupBy_property_entity_projecting_collection_and_single_result(async); - - AssertSql( - """ -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 o1 ON c."CustomerID" = o1."CustomerID" -LEFT JOIN ( - 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 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 -"""); - } - - public override async Task Final_GroupBy_TagWith(bool async) - { - await base.Final_GroupBy_TagWith(async); - - AssertSql( - """ --- foo - -SELECT c."City", c."CustomerID", c."Address", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region" -FROM "Customers" AS c -ORDER BY c."City" NULLS FIRST -"""); - } - - #endregion Final - - #region GroupByWithoutAggregate - - public override async Task GroupBy_Where_with_grouping_result(bool async) - { - await base.GroupBy_Where_with_grouping_result(async); - - AssertSql(); - } - - public override async Task GroupBy_OrderBy_with_grouping_result(bool async) - { - await base.GroupBy_OrderBy_with_grouping_result(async); - - AssertSql(); - } - - public override async Task GroupBy_SelectMany(bool async) - { - await base.GroupBy_SelectMany(async); - - AssertSql(); - } - - public override async Task OrderBy_GroupBy_SelectMany(bool async) - { - await base.OrderBy_GroupBy_SelectMany(async); - - AssertSql(); - } - - public override async Task OrderBy_GroupBy_SelectMany_shadow(bool async) - { - await base.OrderBy_GroupBy_SelectMany_shadow(async); - - AssertSql(); - } - - public override async Task GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(bool async) - { - await base.GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(async); - - AssertSql(); - } - - public override async Task GroupBy_Distinct(bool async) - { - await base.GroupBy_Distinct(async); - - AssertSql(); - } - - public override async Task GroupBy_complex_key_without_aggregate(bool async) - { - await base.GroupBy_complex_key_without_aggregate(async); - - AssertSql( - """ -SELECT s1."Key", s3."OrderID", s3."CustomerID", s3."EmployeeID", s3."OrderDate", s3."CustomerID0" -FROM ( - 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 s - GROUP BY s."Key" -) AS s1 -LEFT JOIN ( - SELECT s2."OrderID", s2."CustomerID", s2."EmployeeID", s2."OrderDate", s2."CustomerID0", s2."Key" - FROM ( - 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 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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task GroupBy_OrderBy_Average(bool async) - { - await AssertQueryScalar( - async, - ss => from o in ss.Set() - group o by new { o.CustomerID } - into g - select g.OrderBy(e => e.OrderID).Select(e => (int?)e.OrderID).Average()); - - AssertSql( - """ -SELECT avg(o."OrderID"::double precision ORDER BY o."OrderID" NULLS FIRST) -FROM "Orders" AS o -GROUP BY o."CustomerID" -"""); - } - - #endregion GroupByAndDistinctWithCorrelatedCollection - - // See aggregate tests over TimeSpan in GearsOfWarQueryNpsgqlTest - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryNpgsqlTest.cs deleted file mode 100644 index 8b87c833c6..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindIncludeNoTrackingQueryNpgsqlTest : NorthwindIncludeNoTrackingQueryTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindIncludeNoTrackingQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public override async Task Include_collection_with_last_no_orderby(bool async) - => Assert.Equal( - RelationalStrings.LastUsedWithoutOrderBy(nameof(Enumerable.Last)), - (await Assert.ThrowsAsync( - () => base.Include_collection_with_last_no_orderby(async))).Message); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindIncludeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindIncludeQueryNpgsqlTest.cs deleted file mode 100644 index 64cdef5736..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindIncludeQueryNpgsqlTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindIncludeQueryNpgsqlTest : NorthwindIncludeQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindIncludeQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs deleted file mode 100644 index 3d9cbb7e68..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindJoinQueryNpgsqlTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindJoinQueryNpgsqlTest : NorthwindJoinQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindJoinQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // #2759 - public override Task Join_local_collection_int_closure_is_cached_correctly(bool async) - => base.Join_local_collection_int_closure_is_cached_correctly(async); - // => Assert.ThrowsAsync(() => base.Join_local_collection_int_closure_is_cached_correctly(async)); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindKeylessEntitiesQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindKeylessEntitiesQueryNpgsqlTest.cs deleted file mode 100644 index 88ba01178f..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindKeylessEntitiesQueryNpgsqlTest.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindKeylessEntitiesQueryNpgsqlTest : NorthwindKeylessEntitiesQueryRelationalTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindKeylessEntitiesQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task KeylessEntity_with_nav_defining_query(bool async) - { - // FromSql mapping. Issue #21627. - await Assert.ThrowsAsync(() => base.KeylessEntity_with_nav_defining_query(async)); - - AssertSql( - """ -SELECT c."CompanyName", c."OrderCount", c."SearchTerm" -FROM "CustomerQueryWithQueryFilter" AS c -WHERE c."OrderCount" > 0 -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs deleted file mode 100644 index 68c5db3d0d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs +++ /dev/null @@ -1,509 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class NorthwindMiscellaneousQueryNpgsqlTest : NorthwindMiscellaneousQueryRelationalTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindMiscellaneousQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Query_expression_with_to_string_and_contains(bool async) - { - await base.Query_expression_with_to_string_and_contains(async); - - AssertSql( - """ -SELECT o."CustomerID" -FROM "Orders" AS o -WHERE o."OrderDate" IS NOT NULL AND COALESCE(o."EmployeeID"::text, '') LIKE '%7%' -"""); - } - - public override async Task Select_expression_date_add_year(bool async) - { - await base.Select_expression_date_add_year(async); - - AssertSql( - """ -SELECT o."OrderDate" + INTERVAL '1 years' AS "OrderDate" -FROM "Orders" AS o -WHERE o."OrderDate" IS NOT NULL -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task Select_expression_date_add_year_param(bool async) - { - var years = 2; - - await AssertQuery( - async, - ss => ss.Set().Where(o => o.OrderDate != null) - .Select( - o => new Order { OrderDate = o.OrderDate.Value.AddYears(years) }), - e => e.OrderDate); - - AssertSql( - """ -@years='2' - -SELECT o."OrderDate" + CAST(@years::text || ' years' AS interval) AS "OrderDate" -FROM "Orders" AS o -WHERE o."OrderDate" IS NOT NULL -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task DateTime_subtract_TimeSpan(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(o => o.OrderDate - TimeSpan.FromDays(1) == new DateTime(1997, 10, 8))); - - AssertSql( - """ -SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Orders" AS o -WHERE o."OrderDate" - INTERVAL '1 00:00:00' = TIMESTAMP '1997-10-08T00:00:00' -"""); - } - - [Theory] - [MemberData(nameof(IsAsyncData))] - public async Task DateTimeFunction_subtract_DateTime(bool async) - { - await AssertFirst( - async, - ss => ss.Set().Where(o => o.OrderDate != null) - .Select(o => new { Elapsed = (DateTime.Today - ((DateTime)o.OrderDate).Date).Days })); - - AssertSql( - """ -SELECT floor(date_part('day', date_trunc('day', now()::timestamp) - date_trunc('day', o."OrderDate")))::int AS "Elapsed" -FROM "Orders" AS o -WHERE o."OrderDate" IS NOT NULL -LIMIT 1 -"""); - } - - public override Task Add_minutes_on_constant_value(bool async) - => AssertQuery( - async, - ss => ss.Set().Where(c => c.OrderID < 10500) - .OrderBy(o => o.OrderID) - .Select(o => new { Test = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(o.OrderID % 25) }), - assertOrder: true, - elementAsserter: (e, a) => AssertEqual(e.Test, a.Test)); - - public override async Task Client_code_using_instance_method_throws(bool async) - { - Assert.Equal( - CoreStrings.ClientProjectionCapturingConstantInMethodInstance( - "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryNpgsqlTest", - "InstanceMethod"), - (await Assert.ThrowsAsync( - () => base.Client_code_using_instance_method_throws(async))).Message); - - AssertSql(); - } - - public override async Task Client_code_using_instance_in_static_method(bool async) - { - Assert.Equal( - CoreStrings.ClientProjectionCapturingConstantInMethodArgument( - "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryNpgsqlTest", - "StaticMethod"), - (await Assert.ThrowsAsync( - () => base.Client_code_using_instance_in_static_method(async))).Message); - - AssertSql(); - } - - public override async Task Client_code_using_instance_in_anonymous_type(bool async) - { - Assert.Equal( - CoreStrings.ClientProjectionCapturingConstantInTree( - "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryNpgsqlTest"), - (await Assert.ThrowsAsync( - () => base.Client_code_using_instance_in_anonymous_type(async))).Message); - - AssertSql(); - } - - public override async Task Client_code_unknown_method(bool async) - { - await AssertTranslationFailedWithDetails( - () => base.Client_code_unknown_method(async), - CoreStrings.QueryUnableToTranslateMethod( - "Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryTestBase>", - nameof(UnknownMethod))); - - AssertSql(); - } - - public override async Task Max_on_empty_sequence_throws(bool async) - { - await Assert.ThrowsAsync(() => base.Max_on_empty_sequence_throws(async)); - - AssertSql( - """ -SELECT ( - SELECT max(o."OrderID") - FROM "Orders" AS o - WHERE c."CustomerID" = o."CustomerID") AS "Max" -FROM "Customers" AS c -"""); - } - - public override async Task Entity_equality_through_subquery_composite_key(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Entity_equality_through_subquery_composite_key(async))).Message; - - Assert.Equal( - CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported("==", nameof(OrderDetail)), - message); - - AssertSql(); - } - - public override async Task - Select_DTO_constructor_distinct_with_collection_projection_translated_to_server_with_binding_after_client_eval(bool async) - { - // Allow binding of expressions after projection has turned to client eval. Issue #24478. - await Assert.ThrowsAsync( - () => base - .Select_DTO_constructor_distinct_with_collection_projection_translated_to_server_with_binding_after_client_eval(async)); - - AssertSql( - """ -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 o0 -LEFT JOIN "Orders" AS o1 ON o0."CustomerID" = o1."CustomerID" -ORDER BY o0."CustomerID" NULLS FIRST -"""); - } - - // TODO: #3406 - public override Task Where_nanosecond_and_microsecond_component(bool async) - => AssertTranslationFailed(() => base.Where_nanosecond_and_microsecond_component(async)); - - // TODO: Array tests can probably move to the dedicated ArrayQueryTest suite - - #region Array contains - - // Note that this also takes care of array.Any(x => x == y) - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_Contains_constant(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(c => new[] { "ALFKI", "ANATR" }.Contains(c.CustomerID))); - - // Note: for constant lists there's no advantage in using the PostgreSQL-specific x = ANY (a b, c), unlike - // for parameterized lists. - - 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" IN ('ALFKI', 'ANATR') -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_Contains_parameter(bool async) - { - var regions = new[] { "UK", "SP" }; - - await AssertQuery( - async, - ss => ss.Set().Where(c => regions.Contains(c.Region))); - - // Instead of c."Region" IN ('UK', 'SP') we generate the PostgreSQL-specific x = ANY (a, b, c), which can - // be parameterized. - // Ideally parameter sniffing would allow us to produce SQL without the null check since the regions array doesn't contain one - // (see https://github.com/aspnet/EntityFrameworkCore/issues/17598). - AssertSql( - """ -@regions={ 'UK' -'SP' } (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."Region" = ANY (@regions) OR (c."Region" IS NULL AND array_position(@regions, NULL) IS NOT NULL) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_Contains_parameter_with_null(bool async) - { - var regions = new[] { "UK", "SP", null }; - - await AssertQuery( - async, - ss => ss.Set().Where(c => regions.Contains(c.Region))); - - // Instead of c."Region" IN ('UK', 'SP') we generate the PostgreSQL-specific x = ANY (a, b, c), which can - // be parameterized. - // Ideally parameter sniffing would allow us to produce SQL with an optimized null check (no need to check the array server-side) - // (see https://github.com/aspnet/EntityFrameworkCore/issues/17598). - AssertSql( - """ -@regions={ 'UK' -'SP' -NULL } (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."Region" = ANY (@regions) OR (c."Region" IS NULL AND array_position(@regions, NULL) IS NOT NULL) -"""); - } - - #endregion Array contains - - #region Any/All Like - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_Any_Like(bool async) - { - await using var context = CreateContext(); - - var collection = new[] { "A%", "B%", "C%" }; - var query = context.Set().Where(c => collection.Any(y => EF.Functions.Like(c.Address, y))); - var result = async ? await query.ToListAsync() : query.ToList(); - - Assert.Equal( - [ - "ANATR", - "BERGS", - "BOLID", - "CACTU", - "COMMI", - "CONSH", - "FISSA", - "FRANK", - "GODOS", - "GOURL", - "HILAA", - "HUNGC", - "LILAS", - "LINOD", - "PERIC", - "QUEEN", - "RANCH", - "RICAR", - "SUPRD", - "TORTU", - "TRADH", - "WANDK" - ], result.Select(e => e.CustomerID)); - - AssertSql( - """ -@collection={ '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) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_All_Like(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.Empty(result); - - AssertSql( - """ -@collection={ '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) -"""); - } - - [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={ '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 NOT (c."Address" LIKE ALL (@collection)) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_Any_ILike(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( - [ - "ANATR", - "BERGS", - "BOLID", - "CACTU", - "COMMI", - "CONSH", - "FISSA", - "FRANK", - "GODOS", - "GOURL", - "HILAA", - "HUNGC", - "LILAS", - "LINOD", - "PERIC", - "QUEEN", - "RANCH", - "RICAR", - "SUPRD", - "TORTU", - "TRADH", - "WANDK" - ], result.Select(e => e.CustomerID)); - - AssertSql( - """ -@collection={ '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) -"""); - } - - [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={ '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 NOT (c."Address" ILIKE ANY (@collection)) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Array_All_ILike(bool async) - { - await using var context = CreateContext(); - - var collection = new[] { "a%", "b%", "c%" }; - var query = context.Set().Where(c => collection.All(y => EF.Functions.ILike(c.Address, y))); - var result = async ? await query.ToListAsync() : query.ToList(); - - Assert.Empty(result); - - AssertSql( - """ -@collection={ '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) -"""); - } - - #endregion Any/All Like - - [ConditionalFact] // #1560 - public async Task Lateral_join_with_table_is_rewritten_with_subquery() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers.Select(c1 => ctx.Customers.Select(c2 => c2.ContactName).ToList()).ToListAsync(); - - AssertSql( - """ -SELECT c."CustomerID", c0."ContactName", c0."CustomerID" -FROM "Customers" AS c -LEFT JOIN LATERAL (SELECT * FROM "Customers") AS c0 ON TRUE -ORDER BY c."CustomerID" NULLS FIRST -"""); - } - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindNavigationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindNavigationsQueryNpgsqlTest.cs deleted file mode 100644 index 2c2c913e02..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindNavigationsQueryNpgsqlTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindNavigationsQueryNpgsqlTest : NorthwindNavigationsQueryRelationalTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindNavigationsQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryFiltersQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryFiltersQueryNpgsqlTest.cs deleted file mode 100644 index 62fe544506..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryFiltersQueryNpgsqlTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindQueryFiltersQueryNpgsqlTest : NorthwindQueryFiltersQueryTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindQueryFiltersQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs deleted file mode 100644 index 891ce2f056..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryNpgsqlFixture.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindQueryNpgsqlFixture : NorthwindQueryRelationalFixture - where TModelCustomizer : ITestModelCustomizer, new() -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlNorthwindTestStoreFactory.Instance; - - protected override Type ContextType - => typeof(NorthwindNpgsqlContext); - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder).SetPostgresVersion(TestEnvironment.PostgresVersion); - return optionsBuilder; - } - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // Note that we map price properties to numeric(12,2) columns, not to money as in SqlServer, since in - // PG, money is discouraged/obsolete and various tests fail with it. - - modelBuilder.Entity( - b => - { - b.Property(o => o.EmployeeID).HasColumnType("int"); - b.Property(o => o.OrderDate).HasColumnType("timestamp without time zone"); - }); - - modelBuilder.Entity( - b => - { - b.Property(c => c.EmployeeID).HasColumnType("int"); - b.Property(c => c.ReportsTo).HasColumnType("int"); - }); - - modelBuilder.Entity() - .Property(o => o.EmployeeID) - .HasColumnType("int"); - - modelBuilder.Entity() - .Property(p => p.UnitsInStock) - .HasColumnType("smallint"); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryTaggingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryTaggingQueryNpgsqlTest.cs deleted file mode 100644 index 911afe2082..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindQueryTaggingQueryNpgsqlTest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindQueryTaggingQueryNpgsqlTest : NorthwindQueryTaggingQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindQueryTaggingQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSelectQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSelectQueryNpgsqlTest.cs deleted file mode 100644 index 6b04993092..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSelectQueryNpgsqlTest.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindSelectQueryNpgsqlTest : NorthwindSelectQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindSelectQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Select_datetime_DayOfWeek_component(bool async) - { - await base.Select_datetime_DayOfWeek_component(async); - - AssertSql( - """ -SELECT floor(date_part('dow', o."OrderDate"))::int -FROM "Orders" AS o -"""); - } - - public override async Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) - { - // Identifier set for Distinct. Issue #24440. - Assert.Equal( - RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, - (await Assert.ThrowsAsync( - () => base.Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(async))) - .Message); - - AssertSql(); - } - - public override async Task - SelectMany_with_collection_being_correlated_subquery_which_references_non_mapped_properties_from_inner_and_outer_entity( - bool async) - { - await AssertUnableToTranslateEFProperty( - () => base - .SelectMany_with_collection_being_correlated_subquery_which_references_non_mapped_properties_from_inner_and_outer_entity( - async)); - - AssertSql(); - } - - public override Task Member_binding_after_ctor_arguments_fails_with_client_eval(bool async) - => AssertTranslationFailed(() => base.Member_binding_after_ctor_arguments_fails_with_client_eval(async)); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs deleted file mode 100644 index bf651e663d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSetOperationsQueryNpgsqlTest.cs +++ /dev/null @@ -1,195 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindSetOperationsQueryNpgsqlTest - : NorthwindSetOperationsQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindSetOperationsQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Leftmost_nulls_with_two_unions(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) - .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); - - AssertSql( - """ -SELECT NULL AS "OrderID", o."CustomerID" -FROM "Orders" AS o -UNION -SELECT o0."OrderID", o0."CustomerID" -FROM "Orders" AS o0 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Leftmost_nulls_with_three_unions(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); - - AssertSql( - """ -SELECT NULL::int AS "OrderID", o."CustomerID" -FROM "Orders" AS o -UNION -SELECT NULL AS "OrderID", o0."CustomerID" -FROM "Orders" AS o0 -UNION -SELECT o1."OrderID", o1."CustomerID" -FROM "Orders" AS o1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Leftmost_nulls_with_four_unions(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); - - AssertSql( - """ -SELECT NULL::int AS "OrderID", o."CustomerID" -FROM "Orders" AS o -UNION -SELECT NULL AS "OrderID", o0."CustomerID" -FROM "Orders" AS o0 -UNION -SELECT NULL AS "OrderID", o1."CustomerID" -FROM "Orders" AS o1 -UNION -SELECT o2."OrderID", o2."CustomerID" -FROM "Orders" AS o2 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Leftmost_nulls_with_five_unions(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID }))); - - AssertSql( - """ -SELECT NULL::int AS "OrderID", o."CustomerID" -FROM "Orders" AS o -UNION -SELECT NULL AS "OrderID", o0."CustomerID" -FROM "Orders" AS o0 -UNION -SELECT NULL AS "OrderID", o1."CustomerID" -FROM "Orders" AS o1 -UNION -SELECT NULL AS "OrderID", o2."CustomerID" -FROM "Orders" AS o2 -UNION -SELECT o3."OrderID", o3."CustomerID" -FROM "Orders" AS o3 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Leftmost_nulls_in_tables_and_predicate(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID })) - .Where( - o => o.CustomerID - == ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID }) - .Union(ss.Set().Select(o => new { OrderID = (int?)null, o.CustomerID })) - .Union(ss.Set().Select(o => new { OrderID = (int?)o.OrderID, o.CustomerID })) - .OrderBy(o => o.CustomerID) - .First() - .CustomerID)); - - AssertSql( - """ -SELECT u0."OrderID", u0."CustomerID" -FROM ( - SELECT NULL::int AS "OrderID", o."CustomerID" - FROM "Orders" AS o - UNION - SELECT NULL AS "OrderID", o0."CustomerID" - FROM "Orders" AS o0 - UNION - SELECT o1."OrderID", o1."CustomerID" - FROM "Orders" AS o1 -) AS u0 -WHERE u0."CustomerID" = ( - SELECT u2."CustomerID" - FROM ( - SELECT NULL::int AS "OrderID", o2."CustomerID" - FROM "Orders" AS o2 - UNION - SELECT NULL AS "OrderID", o3."CustomerID" - FROM "Orders" AS o3 - UNION - SELECT o4."OrderID", o4."CustomerID" - FROM "Orders" AS o4 - ) 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 - UNION - SELECT NULL AS "OrderID", o3."CustomerID" - FROM "Orders" AS o3 - UNION - SELECT o4."OrderID", o4."CustomerID" - FROM "Orders" AS o4 - ) AS u2 - ORDER BY u2."CustomerID" NULLS FIRST - LIMIT 1) IS NULL) -"""); - } - - public override async Task Client_eval_Union_FirstOrDefault(bool async) - { - // Client evaluation in projection. Issue #16243. - Assert.Equal( - RelationalStrings.SetOperationsNotAllowedAfterClientEvaluation, - (await Assert.ThrowsAsync( - () => base.Client_eval_Union_FirstOrDefault(async))).Message); - - AssertSql(); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQueryNpgsqlTest.cs deleted file mode 100644 index daa9f53184..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindSplitIncludeNoTrackingQueryNpgsqlTest : NorthwindSplitIncludeNoTrackingQueryTestBase< - NorthwindQueryNpgsqlFixture> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindSplitIncludeNoTrackingQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - // TestSqlLoggerFactory.CaptureOutput(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSplitIncludeQueryNpgsqleTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSplitIncludeQueryNpgsqleTest.cs deleted file mode 100644 index 221785b5b7..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSplitIncludeQueryNpgsqleTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindSplitIncludeQueryNpgsqlTest : NorthwindSplitIncludeQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindSplitIncludeQueryNpgsqlTest( - NorthwindQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - // TestSqlLoggerFactory.CaptureOutput(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs deleted file mode 100644 index 6689159f8d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindSqlQueryNpgsqlTest.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Data.Common; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindSqlQueryNpgsqlTest : NorthwindSqlQueryTestBase> -{ - public NorthwindSqlQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task SqlQueryRaw_over_int(bool async) - { - await base.SqlQueryRaw_over_int(async); - - AssertSql( - """ -SELECT "ProductID" FROM "Products" -"""); - } - - public override async Task SqlQuery_composed_Contains(bool async) - { - await base.SqlQuery_composed_Contains(async); - - AssertSql( - """ -SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" -FROM "Orders" AS o -WHERE o."OrderID" IN ( - SELECT s."Value" - FROM ( - SELECT "ProductID" AS "Value" FROM "Products" - ) AS s -) -"""); - } - - public override async Task SqlQuery_composed_Join(bool async) - { - await base.SqlQuery_composed_Join(async); - - AssertSql( - """ -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 s ON o."OrderID" = s."Value"::int -"""); - } - - public override async Task SqlQuery_over_int_with_parameter(bool async) - { - await base.SqlQuery_over_int_with_parameter(async); - - AssertSql( - """ -p0='10' - -SELECT "ProductID" FROM "Products" WHERE "ProductID" = @p0 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - protected override DbParameter CreateDbParameter(string name, object value) - => new NpgsqlParameter { ParameterName = name, Value = value }; - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindStringIncludeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindStringIncludeQueryNpgsqlTest.cs deleted file mode 100644 index 68f49f44e4..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindStringIncludeQueryNpgsqlTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindStringIncludeQueryNpgsqlTest - : NorthwindStringIncludeQueryTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindStringIncludeQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public override async Task Include_collection_with_last_no_orderby(bool async) - => Assert.Equal( - RelationalStrings.LastUsedWithoutOrderBy(nameof(Enumerable.Last)), - (await Assert.ThrowsAsync( - () => base.Include_collection_with_last_no_orderby(async))).Message); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs deleted file mode 100644 index 63b709e0ab..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs +++ /dev/null @@ -1,379 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class NorthwindWhereQueryNpgsqlTest : NorthwindWhereQueryRelationalTestBase> -{ - // ReSharper disable once UnusedParameter.Local - public NorthwindWhereQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Where_compare_constructed_equal(bool async) - { - // Anonymous type to constant comparison. Issue #14672. - await AssertTranslationFailed(() => base.Where_compare_constructed_equal(async)); - - AssertSql(); - } - - public override async Task Where_compare_constructed_multi_value_equal(bool async) - { - // Anonymous type to constant comparison. Issue #14672. - await AssertTranslationFailed(() => base.Where_compare_constructed_multi_value_equal(async)); - - AssertSql(); - } - - public override async Task Where_compare_constructed_multi_value_not_equal(bool async) - { - // Anonymous type to constant comparison. Issue #14672. - await AssertTranslationFailed(() => base.Where_compare_constructed_multi_value_not_equal(async)); - - AssertSql(); - } - - public override async Task Where_compare_tuple_constructed_equal(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(c => new Tuple(c.City).Equals(new Tuple("London")))); - - 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."City") = ('London') -"""); - } - - public override async Task Where_compare_tuple_constructed_multi_value_equal(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - c => new Tuple(c.City, c.Country).Equals(new Tuple("Sao Paulo", "Brazil")))); - - 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."City", c."Country") = ('Sao Paulo', 'Brazil') -"""); - } - - public override async Task Where_compare_tuple_constructed_multi_value_not_equal(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - c => !new Tuple(c.City, c.Country).Equals(new Tuple("Sao Paulo", "Brazil")))); - - 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."City" <> 'Sao Paulo' OR c."City" IS NULL OR c."Country" <> 'Brazil' OR c."Country" IS NULL -"""); - } - - public override async Task Where_compare_tuple_create_constructed_equal(bool async) - { - // Anonymous type to constant comparison. Issue #14672. - await AssertTranslationFailed(() => base.Where_compare_tuple_create_constructed_equal(async)); - - AssertSql(); - } - - public override async Task Where_compare_tuple_create_constructed_multi_value_equal(bool async) - { - // Anonymous type to constant comparison. Issue #14672. - await AssertTranslationFailed(() => base.Where_compare_tuple_create_constructed_multi_value_equal(async)); - - AssertSql(); - } - - public override async Task Where_compare_tuple_create_constructed_multi_value_not_equal(bool async) - { - // Anonymous type to constant comparison. Issue #14672. - await AssertTranslationFailed(() => base.Where_compare_tuple_create_constructed_multi_value_not_equal(async)); - - AssertSql(); - } - - #region Row values - - [ConditionalFact] - public async Task Row_value_GreaterThan() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers - .Where( - c => EF.Functions.GreaterThan( - ValueTuple.Create(c.City, c.CustomerID), - ValueTuple.Create("Buenos Aires", "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") > ('Buenos Aires', 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_GreaterThan_with_differing_types() - { - await using var ctx = CreateContext(); - - _ = await ctx.Orders - .Where( - o => EF.Functions.GreaterThan( - ValueTuple.Create(o.CustomerID, o.OrderID), - ValueTuple.Create("ALFKI", 10702))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Orders" AS o -WHERE (o."CustomerID", o."OrderID") > ('ALFKI', 10702) -"""); - } - - [ConditionalFact] - public async Task Row_value_GreaterThan_with_parameter() - { - await using var ctx = CreateContext(); - - var city1 = "Buenos Aires"; - - _ = await ctx.Customers - .Where( - c => EF.Functions.GreaterThan( - ValueTuple.Create(c.City, c.CustomerID), - ValueTuple.Create(city1, "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -@city1='Buenos Aires' - -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") > (@city1, 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_GreaterThan_with_parameter_with_ValueTuple_constructor() - { - await using var ctx = CreateContext(); - - var city1 = "Buenos Aires"; - - _ = await ctx.Customers - .Where( - c => EF.Functions.GreaterThan( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple(city1, "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -@city1='Buenos Aires' - -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") > (@city1, 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_LessThan() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers - .Where( - c => EF.Functions.LessThan( - ValueTuple.Create(c.City, c.CustomerID), - ValueTuple.Create("Buenos Aires", "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") < ('Buenos Aires', 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_GreaterThanOrEqual() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers - .Where( - c => EF.Functions.GreaterThanOrEqual( - ValueTuple.Create(c.City, c.CustomerID), - ValueTuple.Create("Buenos Aires", "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") >= ('Buenos Aires', 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_LessThanOrEqual() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers - .Where( - c => EF.Functions.LessThanOrEqual( - ValueTuple.Create(c.City, c.CustomerID), - ValueTuple.Create("Buenos Aires", "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") <= ('Buenos Aires', 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_with_ValueTuple_constructor() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers - .Where( - c => EF.Functions.GreaterThan( - new ValueTuple(c.City, c.CustomerID), - new ValueTuple("Buenos Aires", "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") > ('Buenos Aires', 'OCEAN') -"""); - } - - [ConditionalFact] - public async Task Row_value_parameter_count_mismatch() - { - await using var ctx = CreateContext(); - - var exception = await Assert.ThrowsAsync( - () => ctx.Customers - .Where( - c => EF.Functions.LessThanOrEqual( - ValueTuple.Create(c.City, c.CustomerID), - ValueTuple.Create("Buenos Aires", "OCEAN", "foo"))) - .CountAsync()); - - Assert.Equal(NpgsqlStrings.RowValueComparisonRequiresTuplesOfSameLength, exception.Message); - } - - [ConditionalFact] - public async Task Row_value_equals() - { - await using var ctx = CreateContext(); - - _ = await ctx.Customers - .Where( - c => - ValueTuple.Create(c.City, c.CustomerID).Equals( - ValueTuple.Create("Buenos Aires", "OCEAN"))) - .CountAsync(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE (c."City", c."CustomerID") = ('Buenos Aires', 'OCEAN') -"""); - } - - [ConditionalFact] - 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 -FROM "Customers" AS c -WHERE c."CustomerID" <> 'OCEAN' OR c."City" <> 'Buenos Aires' OR c."City" IS NULL -"""); - } - - [ConditionalFact] - public async Task Row_value_project() - { - await using var ctx = CreateContext(); - - var (customerId, orderDate) = await ctx.Orders - .Where(o => o.OrderID == 10248) - .Select(o => ValueTuple.Create(o.CustomerID, o.OrderDate)) - .SingleAsync(); - - Assert.Equal("VINET", customerId); - Assert.Equal(new DateTime(1996, 7, 4), orderDate); - - AssertSql( - """ -SELECT (o."CustomerID", o."OrderDate") -FROM "Orders" AS o -WHERE o."OrderID" = 10248 -LIMIT 2 -"""); - } - - #endregion Row values - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs deleted file mode 100644 index d9bf342ffe..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NullKeysNpgsqlTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class NullKeysNpgsqlTest(NullKeysNpgsqlTest.NullKeysNpgsqlFixture fixture) - : NullKeysTestBase(fixture) -{ - public class NullKeysNpgsqlFixture : NullKeysFixtureBase - { - protected override string StoreName { get; } = "StringsContext"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs deleted file mode 100644 index 8e88dc16f4..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NullSemanticsQueryNpgsqlTest.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.NullSemanticsModel; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Microsoft.EntityFrameworkCore.Query; - -// ReSharper disable once UnusedMember.Global -public class NullSemanticsQueryNpgsqlTest : NullSemanticsQueryTestBase -{ - public NullSemanticsQueryNpgsqlTest(NullSemanticsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Compare_row_values_equal_without_expansion(bool async) - { - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.NullableStringB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => ValueTuple.Create(e.IntA, e.NullableStringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) - .Select(e => e.Id)); - - AssertSql( - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."StringA") = (e."IntB", e."StringB") -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."StringA") = (e."IntB", e."NullableStringB") -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."NullableStringA") = (e."IntB", e."StringB") -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Compare_row_values_equal_with_expansion(bool async) - { - await AssertQueryScalar( - async, ss => ss.Set() - .Where( - e => ValueTuple.Create(e.NullableStringA, e.IntA, e.BoolA) - .Equals(ValueTuple.Create(e.NullableStringB, e.IntB, e.BoolB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where( - e => ValueTuple.Create(e.IntA, e.NullableStringA, e.BoolA) - .Equals(ValueTuple.Create(e.IntB, e.NullableStringB, e.BoolB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where( - e => ValueTuple.Create(e.IntA, e.StringA, e.NullableBoolA) - .Equals(ValueTuple.Create(e.IntB, e.StringB, e.NullableBoolB))) - .Select(e => e.Id)); - - AssertSql( - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."BoolA") = (e."IntB", e."BoolB") AND (e."NullableStringA" = e."NullableStringB" OR (e."NullableStringA" IS NULL AND e."NullableStringB" IS NULL)) -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."BoolA") = (e."IntB", e."BoolB") AND (e."NullableStringA" = e."NullableStringB" OR (e."NullableStringA" IS NULL AND e."NullableStringB" IS NULL)) -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."StringA") = (e."IntB", e."StringB") AND (e."NullableBoolA" = e."NullableBoolB" OR (e."NullableBoolA" IS NULL AND e."NullableBoolB" IS NULL)) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Compare_row_values_not_equal(bool async) - { - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => !ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => !ValueTuple.Create(e.IntA, e.StringA).Equals(ValueTuple.Create(e.IntB, e.NullableStringB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => !ValueTuple.Create(e.IntA, e.NullableStringA).Equals(ValueTuple.Create(e.IntB, e.StringB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where(e => !ValueTuple.Create(e.IntA, e.NullableStringA).Equals(ValueTuple.Create(e.IntB, e.NullableStringB))) - .Select(e => e.Id)); - - await AssertQueryScalar( - async, ss => ss.Set() - .Where( - e => !ValueTuple.Create(e.IntA, e.StringA, e.NullableBoolA) - .Equals(ValueTuple.Create(e.IntB, e.StringB, e.NullableBoolB))) - .Select(e => e.Id)); - - AssertSql( - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."StringA") <> (e."IntB", e."StringB") -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE e."IntA" <> e."IntB" OR e."StringA" <> e."NullableStringB" OR e."NullableStringB" IS NULL -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE e."IntA" <> e."IntB" OR e."NullableStringA" <> e."StringB" OR e."NullableStringA" IS NULL -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE e."IntA" <> e."IntB" OR ((e."NullableStringA" <> e."NullableStringB" OR e."NullableStringA" IS NULL OR e."NullableStringB" IS NULL) AND (e."NullableStringA" IS NOT NULL OR e."NullableStringB" IS NOT NULL)) -""", - // - """ -SELECT e."Id" -FROM "Entities1" AS e -WHERE (e."IntA", e."StringA") <> (e."IntB", e."StringB") OR ((e."NullableBoolA" <> e."NullableBoolB" OR e."NullableBoolA" IS NULL OR e."NullableBoolB" IS NULL) AND (e."NullableBoolA" IS NOT NULL OR e."NullableBoolB" IS NOT NULL)) -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - protected override NullSemanticsContext CreateContext(bool useRelationalNulls = false) - { - var options = new DbContextOptionsBuilder(Fixture.CreateOptions()); - if (useRelationalNulls) - { - new NpgsqlDbContextOptionsBuilder(options).UseRelationalNulls(); - } - - var context = new NullSemanticsContext(options.Options); - - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - - 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 deleted file mode 100644 index 7d453214f8..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/OperatorsQueryNpgsqlTest.cs +++ /dev/null @@ -1,199 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.Operators; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class OperatorsQueryNpgsqlTest(NonSharedFixture fixture) : OperatorsQueryTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); - - public override async Task Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false() - { - await base.Bitwise_and_on_expression_with_like_and_null_check_being_compared_to_false(); - - 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( - """ -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( - """ -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() - { - await base.Or_on_two_nested_binaries_and_another_simple_comparison(); - - AssertSql( - """ -SELECT o."Id" AS "Id1", o0."Id" AS "Id2", o1."Id" AS "Id3", o2."Id" AS "Id4", o3."Id" AS "Id5" -FROM "OperatorEntityString" AS o -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 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 -"""); - } - - public override async Task Projection_with_not_and_negation_on_integer() - { - await base.Projection_with_not_and_negation_on_integer(); - - AssertSql( - """ -SELECT (~(-(-(o1."Value" + o."Value" + 2)))) % (-(o0."Value" + o0."Value") - o."Value") -FROM "OperatorEntityLong" AS o -CROSS JOIN "OperatorEntityLong" AS o0 -CROSS JOIN "OperatorEntityLong" AS o1 -ORDER BY o."Id" NULLS FIRST, o0."Id" NULLS FIRST, o1."Id" NULLS FIRST -"""); - } - - public override async Task Negate_on_column(bool async) - { - await base.Negate_on_column(async); - - AssertSql( - """ -SELECT o."Id" -FROM "OperatorEntityInt" AS o -WHERE o."Id" = -o."Value" -"""); - } - - public override async Task Double_negate_on_column() - { - await base.Double_negate_on_column(); - - AssertSql( - """ -SELECT o."Id" -FROM "OperatorEntityInt" AS o -WHERE -(-o."Value") = o."Value" -"""); - } - - public override async Task Negate_on_binary_expression(bool async) - { - await base.Negate_on_binary_expression(async); - - AssertSql( - """ -SELECT o."Id" AS "Id1", o0."Id" AS "Id2" -FROM "OperatorEntityInt" AS o -CROSS JOIN "OperatorEntityInt" AS o0 -WHERE -o."Value" = -(o."Id" + o0."Value") -"""); - } - - public override async Task Negate_on_like_expression(bool async) - { - await base.Negate_on_like_expression(async); - - AssertSql( - """ -SELECT o."Id" -FROM "OperatorEntityString" AS o -WHERE o."Value" NOT LIKE 'A%' OR o."Value" IS NULL -"""); - } - - public override async Task Concat_and_json_scalar(bool async) - { - await base.Concat_and_json_scalar(async); - - AssertSql( - """ -SELECT o."Id", o."Owned" -FROM "Owner" AS o -WHERE 'Foo' || (o."Owned" ->> 'SomeProperty') = 'FooBar' -LIMIT 2 -"""); - } - - [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); - ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableInt); - ctx.Set().AddRange(ExpectedData.OperatorEntitiesLong); - ctx.Set().AddRange(ExpectedData.OperatorEntitiesBool); - ctx.Set().AddRange(ExpectedData.OperatorEntitiesNullableBool); - // ctx.Set().AddRange(ExpectedData.OperatorEntitiesDateTimeOffset); - - await ctx.SaveChangesAsync(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs deleted file mode 100644 index fec4dbf935..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/OptionalDependentQueryNpgsqlTest.cs +++ /dev/null @@ -1,159 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.OptionalDependent; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class OptionalDependentQueryNpgsqlTest : OptionalDependentQueryTestBase< - OptionalDependentQueryNpgsqlTest.OptionalDependentQueryNpgsqlFixture> -{ - public OptionalDependentQueryNpgsqlTest(OptionalDependentQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Basic_projection_entity_with_all_optional(bool async) - { - await base.Basic_projection_entity_with_all_optional(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesAllOptional" AS e -"""); - } - - public override async Task Basic_projection_entity_with_some_required(bool async) - { - await base.Basic_projection_entity_with_some_required(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesSomeRequired" AS e -"""); - } - - public override async Task Filter_optional_dependent_with_all_optional_compared_to_null(bool async) - { - await base.Filter_optional_dependent_with_all_optional_compared_to_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesAllOptional" AS e -WHERE (e."Json") IS NULL -"""); - } - - public override async Task Filter_optional_dependent_with_all_optional_compared_to_not_null(bool async) - { - await base.Filter_optional_dependent_with_all_optional_compared_to_not_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesAllOptional" AS e -WHERE (e."Json") IS NOT NULL -"""); - } - - public override async Task Filter_optional_dependent_with_some_required_compared_to_null(bool async) - { - await base.Filter_optional_dependent_with_some_required_compared_to_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesSomeRequired" AS e -WHERE (e."Json") IS NULL -"""); - } - - public override async Task Filter_optional_dependent_with_some_required_compared_to_not_null(bool async) - { - await base.Filter_optional_dependent_with_some_required_compared_to_not_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesSomeRequired" AS e -WHERE (e."Json") IS NOT NULL -"""); - } - - public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_null(bool async) - { - await base.Filter_nested_optional_dependent_with_all_optional_compared_to_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesAllOptional" AS e -WHERE (e."Json" ->> 'OpNav1') IS NULL -"""); - } - - public override async Task Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(bool async) - { - await base.Filter_nested_optional_dependent_with_all_optional_compared_to_not_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesAllOptional" AS e -WHERE (e."Json" ->> 'OpNav2') IS NOT NULL -"""); - } - - public override async Task Filter_nested_optional_dependent_with_some_required_compared_to_null(bool async) - { - await base.Filter_nested_optional_dependent_with_some_required_compared_to_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesSomeRequired" AS e -WHERE (e."Json" ->> 'ReqNav1') IS NULL -"""); - } - - public override async Task Filter_nested_optional_dependent_with_some_required_compared_to_not_null(bool async) - { - await base.Filter_nested_optional_dependent_with_some_required_compared_to_not_null(async); - - AssertSql( - """ -SELECT e."Id", e."Name", e."Json" -FROM "EntitiesSomeRequired" AS e -WHERE (e."Json" ->> 'ReqNav2') IS NOT NULL -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class OptionalDependentQueryNpgsqlFixture : OptionalDependentQueryFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // The EF seed data has Unspecified DateTimes, but we map DateTime to timestamptz by default, which requires UTC DateTimes. - // Configure the properties to have timestamp, which allows Unspecified DateTimes. - modelBuilder.Entity().OwnsOne( - x => x.Json, - b => b.OwnsOne(x => x.OpNav2, b2 => b2.Property(op => op.ReqNested2).HasColumnType("timestamp without time zone"))); - - modelBuilder.Entity().OwnsOne( - x => x.Json, b => - { - b.OwnsOne(x => x.OpNav2, b2 => b2.Property(op => op.ReqNested2).HasColumnType("timestamp without time zone")); - b.OwnsOne(x => x.ReqNav2, b2 => b2.Property(op => op.ReqNested2).HasColumnType("timestamp without time zone")); - }); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs deleted file mode 100644 index 3f540a3b3e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/OwnedEntityQueryNpgsqlTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class OwnedEntityQueryNpgsqlTest(NonSharedFixture fixture) : OwnedEntityQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs deleted file mode 100644 index 33349f8237..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/OwnedQueryNpgsqlTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class OwnedQueryNpgsqlTest(OwnedQueryNpgsqlTest.OwnedQueryNpgsqlFixture fixture) - : OwnedQueryRelationalTestBase(fixture) -{ - public class OwnedQueryNpgsqlFixture : RelationalOwnedQueryFixture - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity( - eb => eb.OwnsMany( - p => p.Orders, ob => ob.IndexerProperty("OrderDate").HasColumnType("timestamp without time zone"))); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs deleted file mode 100644 index c255150642..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledQueryNpgsqlTest.cs +++ /dev/null @@ -1,2110 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class PrecompiledQueryNpgsqlTest( - PrecompiledQueryNpgsqlTest.PrecompiledQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : PrecompiledQueryRelationalTestBase(fixture, testOutputHelper), - IClassFixture -{ - protected override bool AlwaysPrintGeneratedSources - => false; - - #region Expression types - - public override async Task BinaryExpression() - { - await base.BinaryExpression(); - - AssertSql( - """ -@id='3' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Id" > @id -"""); - } - - 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='yes' - -SELECT CASE - WHEN b."Id" = 2 THEN @yes - 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_startswith='foo%' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Name" LIKE @pattern_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='8' - -SELECT @id 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='8' - -SELECT ARRAY[b."Id",b."Id" + @i]::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 Regular operators - - 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_with_constant() - { - await base.Skip_with_constant(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Name" NULLS FIRST -OFFSET @p -"""); - } - - public override async Task Skip_with_parameter() - { - await base.Skip_with_parameter(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Name" NULLS FIRST -OFFSET @p -"""); - } - - public override async Task Take_with_constant() - { - await base.Take_with_constant(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Name" NULLS FIRST -LIMIT @p -"""); - } - - public override async Task Take_with_parameter() - { - await base.Take_with_parameter(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Name" NULLS FIRST -LIMIT @p -"""); - } - - public override async Task Select_changes_type() - { - await base.Select_changes_type(); - - AssertSql( - """ -SELECT b."Name" -FROM "Blogs" AS b -"""); - } - - public override async Task Select_anonymous_object() - { - await base.Select_anonymous_object(); - - AssertSql( - """ -SELECT COALESCE(b."Name", '') || 'Foo' AS "Foo" -FROM "Blogs" AS b -"""); - } - - 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 -"""); - } - - #endregion Regular operators - - #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='8' - -SELECT @p IN ( - SELECT b."Id" - FROM "Blogs" AS b -) -""", - // - """ -@p='7' - -SELECT @p IN ( - SELECT b."Id" - FROM "Blogs" AS b -) -"""); - } - - public override async Task Terminating_ContainsAsync() - { - await base.Terminating_ContainsAsync(); - - AssertSql( - """ -@p='8' - -SELECT @p IN ( - SELECT b."Id" - FROM "Blogs" AS b -) -""", - // - """ -@p='7' - -SELECT @p 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='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -""", - // - """ -@p='3' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -"""); - } - - public override async Task Terminating_ElementAtAsync() - { - await base.Terminating_ElementAtAsync(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -""", - // - """ -@p='3' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -"""); - } - - public override async Task Terminating_ElementAtOrDefault() - { - await base.Terminating_ElementAtOrDefault(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -""", - // - """ -@p='3' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -"""); - } - - public override async Task Terminating_ElementAtOrDefaultAsync() - { - await base.Terminating_ElementAtOrDefaultAsync(); - - AssertSql( - """ -@p='1' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -""", - // - """ -@p='3' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -ORDER BY b."Id" NULLS FIRST -LIMIT 1 OFFSET @p -"""); - } - - 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_with_lambda() - { - await base.Terminating_ExecuteUpdate_with_lambda(); - - AssertSql( - """ -@suffix='Suffix' - -UPDATE "Blogs" AS b -SET "Name" = COALESCE(b."Name", '') || @suffix -WHERE b."Id" > 8 -""", - // - """ -SELECT count(*)::int -FROM "Blogs" AS b -WHERE b."Id" = 9 AND b."Name" = 'Blog2Suffix' -"""); - } - - public override async Task Terminating_ExecuteUpdate_without_lambda() - { - await base.Terminating_ExecuteUpdate_without_lambda(); - - AssertSql( - """ -@newValue='NewValue' - -UPDATE "Blogs" AS b -SET "Name" = @newValue -WHERE b."Id" > 8 -""", - // - """ -SELECT count(*)::int -FROM "Blogs" AS b -WHERE b."Id" = 9 AND b."Name" = 'NewValue' -"""); - } - - public override async Task Terminating_ExecuteUpdateAsync_with_lambda() - { - await base.Terminating_ExecuteUpdateAsync_with_lambda(); - - AssertSql( - """ -@suffix='Suffix' - -UPDATE "Blogs" AS b -SET "Name" = COALESCE(b."Name", '') || @suffix -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_without_lambda() - { - await base.Terminating_ExecuteUpdateAsync_without_lambda(); - - AssertSql( - """ -@newValue='NewValue' - -UPDATE "Blogs" AS b -SET "Name" = @newValue -WHERE b."Id" > 8 -""", - // - """ -SELECT count(*)::int -FROM "Blogs" AS b -WHERE b."Id" = 9 AND b."Name" = 'NewValue' -"""); - } - - public override async Task Terminating_with_cancellation_token() - { - await base.Terminating_with_cancellation_token(); - - 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 -"""); - } - - #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( - """ -@p={ '1' -'2' -'3' } (DbType = Object) - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Id" = ANY (@p) -"""); - } - - 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 Captured variable handling - - public override async Task Two_captured_variables_in_same_lambda() - { - await base.Two_captured_variables_in_same_lambda(); - - AssertSql( - """ -@yes='yes' -@no='no' - -SELECT CASE - WHEN b."Id" = 3 THEN @yes - ELSE @no -END -FROM "Blogs" AS b -"""); - } - - public override async Task Two_captured_variables_in_different_lambdas() - { - await base.Two_captured_variables_in_different_lambdas(); - - AssertSql( - """ -@starts_startswith='Blog%' -@ends_endswith='%2' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Name" LIKE @starts_startswith AND b."Name" LIKE @ends_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_startswith='X%' -@foo_endswith='%X' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Name" LIKE @foo_startswith AND b."Name" LIKE @foo_endswith -"""); - } - - public override async Task Same_captured_variable_twice_in_different_lambdas() - { - await base.Same_captured_variable_twice_in_different_lambdas(); - - AssertSql( - """ -@foo_startswith='X%' -@foo_endswith='%X' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Name" LIKE @foo_startswith AND b."Name" LIKE @foo_endswith -"""); - } - - public override async Task Multiple_queries_with_captured_variables() - { - await base.Multiple_queries_with_captured_variables(); - - AssertSql( - """ -@id1='8' -@id2='9' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Id" = @id1 OR b."Id" = @id2 -""", - // - """ -@id1='8' - -SELECT b."Id", b."Name", b."Json" -FROM "Blogs" AS b -WHERE b."Id" = @id1 -LIMIT 2 -"""); - } - - #endregion Captured variable handling - - #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 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 deleted file mode 100644 index 8131fada70..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/PrecompiledSqlPregenerationQueryNpgsqlTest.cs +++ /dev/null @@ -1,244 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.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='8' - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Id" = @id -"""); - } - - public override async Task Nullable_value_type() - { - await base.Nullable_value_type(); - - AssertSql( - """ -@id='8' (Nullable = true) - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Id" = @id -"""); - } - - public override async Task Nullable_reference_type() - { - await base.Nullable_reference_type(); - - AssertSql( - """ -@name='bar' - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name -"""); - } - - public override async Task Non_nullable_reference_type() - { - await base.Non_nullable_reference_type(); - - AssertSql( - """ -@name='bar' (Nullable = false) - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name -"""); - } - - public override async Task Nullable_and_non_nullable_value_types() - { - await base.Nullable_and_non_nullable_value_types(); - - AssertSql( - """ -@id1='8' (Nullable = true) -@id2='9' - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Id" = @id1 OR b."Id" = @id2 -"""); - } - - public override async Task Two_nullable_reference_types() - { - await base.Two_nullable_reference_types(); - - AssertSql( - """ -@name1='foo' -@name2='bar' - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name1 OR b."Name" = @name2 -"""); - } - - public override async Task Two_non_nullable_reference_types() - { - await base.Two_non_nullable_reference_types(); - - AssertSql( - """ -@name1='foo' (Nullable = false) -@name2='bar' (Nullable = false) - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name1 OR b."Name" = @name2 -"""); - } - - public override async Task Nullable_and_non_nullable_reference_types() - { - await base.Nullable_and_non_nullable_reference_types(); - - AssertSql( - """ -@name1='foo' -@name2='bar' (Nullable = false) - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name1 OR b."Name" = @name2 -"""); - } - - public override async Task Too_many_nullable_parameters_prevent_pregeneration() - { - await base.Too_many_nullable_parameters_prevent_pregeneration(); - - AssertSql( - """ -@name1='foo' -@name2='bar' -@name3='baz' -@name4='baq' - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name1 OR b."Name" = @name2 OR b."Name" = @name3 OR b."Name" = @name4 -"""); - } - - public override async Task Many_non_nullable_parameters_do_not_prevent_pregeneration() - { - await base.Many_non_nullable_parameters_do_not_prevent_pregeneration(); - - AssertSql( - """ -@name1='foo' (Nullable = false) -@name2='bar' (Nullable = false) -@name3='baz' (Nullable = false) -@name4='baq' (Nullable = false) - -SELECT b."Id", b."Name" -FROM "Blogs" AS b -WHERE b."Name" = @name1 OR b."Name" = @name2 OR b."Name" = @name3 OR b."Name" = @name4 -"""); - } - - #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 deleted file mode 100644 index 773dd5090f..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ /dev/null @@ -1,2299 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class PrimitiveCollectionsQueryNpgsqlTest : PrimitiveCollectionsQueryRelationalTestBase< - PrimitiveCollectionsQueryNpgsqlTest.PrimitiveCollectionsQueryNpgsqlFixture> -{ - public PrimitiveCollectionsQueryNpgsqlTest(PrimitiveCollectionsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Inline_collection_of_ints_Contains() - { - await base.Inline_collection_of_ints_Contains(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" IN (10, 999) -"""); - } - - public override async Task Inline_collection_of_nullable_ints_Contains() - { - await base.Inline_collection_of_nullable_ints_Contains(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableInt" IN (10, 999) -"""); - } - - public override async Task Inline_collection_of_nullable_ints_Contains_null() - { - await base.Inline_collection_of_nullable_ints_Contains_null(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableInt" IS NULL OR p."NullableInt" = 999 -"""); - } - - public override async Task Inline_collection_Count_with_zero_values() - { - await base.Inline_collection_Count_with_zero_values(); - - AssertSql(); - } - - public override async Task Inline_collection_Count_with_one_value() - { - await base.Inline_collection_Count_with_one_value(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (2::int)) AS v("Value") - WHERE v."Value" > p."Id") = 1 -"""); - } - - public override async Task Inline_collection_Count_with_two_values() - { - await base.Inline_collection_Count_with_two_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (2::int), (999)) AS v("Value") - WHERE v."Value" > p."Id") = 1 -"""); - } - - public override async Task Inline_collection_Count_with_three_values() - { - await base.Inline_collection_Count_with_three_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (2::int), (999), (1000)) AS v("Value") - WHERE v."Value" > p."Id") = 2 -"""); - } - - public override async Task Inline_collection_Contains_with_zero_values() - { - await base.Inline_collection_Contains_with_zero_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE FALSE -"""); - } - - public override async Task Inline_collection_Contains_with_one_value() - { - await base.Inline_collection_Contains_with_one_value(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" = 2 -"""); - } - - public override async Task Inline_collection_Contains_with_two_values() - { - await base.Inline_collection_Contains_with_two_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" IN (2, 999) -"""); - } - - public override async Task Inline_collection_Contains_with_three_values() - { - await base.Inline_collection_Contains_with_three_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" IN (2, 999, 1000) -"""); - } - - public override async Task Inline_collection_Contains_with_all_parameters() - { - await base.Inline_collection_Contains_with_all_parameters(); - - AssertSql( - """ -@i='2' -@j='999' - -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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" IN (@i, @j) -"""); - } - - public override async Task Inline_collection_Contains_with_constant_and_parameter() - { - await base.Inline_collection_Contains_with_constant_and_parameter(); - - AssertSql( - """ -@j='999' - -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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" IN (2, @j) -"""); - } - - public override async Task Inline_collection_Contains_with_mixed_value_types() - { - await base.Inline_collection_Contains_with_mixed_value_types(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" IN (999, @i, p."Id", p."Id" + p."Int") -"""); - } - - public override async Task Inline_collection_List_Contains_with_mixed_value_types() - { - await base.Inline_collection_List_Contains_with_mixed_value_types(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" IN (999, @i, p."Id", p."Id" + p."Int") -"""); - } - - public override async Task Inline_collection_Contains_as_Any_with_predicate() - { - await base.Inline_collection_Contains_as_Any_with_predicate(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" IN (2, 999) -"""); - } - - public override async Task Inline_collection_negated_Contains_as_All() - { - await base.Inline_collection_negated_Contains_as_All(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" NOT IN (2, 999) -"""); - } - - public override async Task Inline_collection_Min_with_two_values() - { - await base.Inline_collection_Min_with_two_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE LEAST(30, p."Int") = 30 -"""); - } - - public override async Task Inline_collection_List_Min_with_two_values() - { - await base.Inline_collection_List_Min_with_two_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE LEAST(30, p."Int") = 30 -"""); - } - - public override async Task Inline_collection_Max_with_two_values() - { - await base.Inline_collection_Max_with_two_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE GREATEST(30, p."Int") = 30 -"""); - } - - public override async Task Inline_collection_List_Max_with_two_values() - { - await base.Inline_collection_List_Max_with_two_values(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE GREATEST(30, p."Int") = 30 -"""); - } - - public override async Task Inline_collection_Min_with_three_values() - { - await base.Inline_collection_Min_with_three_values(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE LEAST(30, p."Int", @i) = 25 -"""); - } - - public override async Task Inline_collection_List_Min_with_three_values() - { - await base.Inline_collection_List_Min_with_three_values(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE LEAST(30, p."Int", @i) = 25 -"""); - } - - public override async Task Inline_collection_Max_with_three_values() - { - await base.Inline_collection_Max_with_three_values(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE GREATEST(30, p."Int", @i) = 35 -"""); - } - - public override async Task Inline_collection_List_Max_with_three_values() - { - await base.Inline_collection_List_Max_with_three_values(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE GREATEST(30, p."Int", @i) = 35 -"""); - } - - public override async Task Inline_collection_of_nullable_value_type_Min() - { - await base.Inline_collection_of_nullable_value_type_Min(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE LEAST(30, p."Int", @i) = 25 -"""); - } - - public override async Task Inline_collection_of_nullable_value_type_Max() - { - await base.Inline_collection_of_nullable_value_type_Max(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE GREATEST(30, p."Int", @i) = 35 -"""); - } - - public override async Task Inline_collection_of_nullable_value_type_with_null_Min() - { - await base.Inline_collection_of_nullable_value_type_with_null_Min(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE LEAST(30, p."NullableInt", NULL) = 30 -"""); - } - - public override async Task Inline_collection_of_nullable_value_type_with_null_Max() - { - await base.Inline_collection_of_nullable_value_type_with_null_Max(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE GREATEST(30, p."NullableInt", NULL) = 30 -"""); - } - - public override async Task Inline_collection_with_single_parameter_element_Contains() - { - await base.Inline_collection_with_single_parameter_element_Contains(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" = @i -"""); - } - - public override async Task Inline_collection_with_single_parameter_element_Count() - { - await base.Inline_collection_with_single_parameter_element_Count(); - - AssertSql( - """ -@i='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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (@i::int)) AS v("Value") - WHERE v."Value" > p."Id") = 1 -"""); - } - - public override async Task Inline_collection_Contains_with_EF_Parameter() - { - await base.Inline_collection_Contains_with_EF_Parameter(); - - AssertSql( - """ -@p={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" = ANY (@p) -"""); - } - - public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() - { - await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(); - - AssertSql( - """ -@p={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM unnest(@p) AS p0(value) - WHERE p0.value > p."Id") = 2 -"""); - } - - public override async Task Parameter_collection_Count() - { - await base.Parameter_collection_Count(); - - AssertSql( - """ -@ids={ '2' -'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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM unnest(@ids) AS i(value) - WHERE i.value > p."Id") = 1 -"""); - } - - public override async Task Parameter_collection_of_ints_Contains_int() - { - await base.Parameter_collection_of_ints_Contains_int(); - - AssertSql( - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" = ANY (@ints) -""", - // - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."Int" = ANY (@ints) AND p."Int" = ANY (@ints) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_HashSet_of_ints_Contains_int() - { - await base.Parameter_collection_HashSet_of_ints_Contains_int(); - - AssertSql( - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" = ANY (@ints) -""", - // - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."Int" = ANY (@ints) AND p."Int" = ANY (@ints) IS NOT NULL) -"""); - } - - [ConditionalFact] - public virtual async Task Parameter_collection_HashSet_with_value_converter_Contains() - { - HashSet enums = [MyEnum.Value1, MyEnum.Value4]; - - await AssertQuery(ss => ss.Set().Where(c => enums.Contains(c.Enum))); - - AssertSql( - """ -@enums={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Enum" = ANY (@enums) -"""); - } - - [ConditionalFact] - public virtual async Task Parameter_collection_Dictionary_Values_with_value_converter_Contains() - { - Dictionary enums = new() - { - [0] = MyEnum.Value1, - [1] = MyEnum.Value4 - }; - - // Dictionary<>.ValuesCollection doesn't have a public parameterless constructor, so NpgsqlArrayConverter can't convert to it - // (see #3050). We still allow NpgsqlArrayConverter to be built to allow one-directional conversion: in the query below, - // we only need to write enum.Values as a parameter (never read it). - await AssertQuery(ss => ss.Set().Where(c => enums.Values.Contains(c.Enum))); - - AssertSql( - """ -@enums_Values={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Enum" = ANY (@enums_Values) -"""); - } - - public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int() - { - await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(); - - AssertSql( - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" = ANY (@ints) -""", - // - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."Int" = ANY (@ints) AND p."Int" = ANY (@ints) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_of_ints_Contains_nullable_int() - { - await base.Parameter_collection_of_ints_Contains_nullable_int(); - - AssertSql( - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableInt" = ANY (@ints) OR (p."NullableInt" IS NULL AND array_position(@ints, NULL) IS NOT NULL) -""", - // - """ -@ints={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."NullableInt" = ANY (@ints) AND p."NullableInt" = ANY (@ints) IS NOT NULL) AND (p."NullableInt" IS NOT NULL OR array_position(@ints, NULL) IS NULL) -"""); - } - - public override async Task Parameter_collection_of_nullable_ints_Contains_int() - { - await base.Parameter_collection_of_nullable_ints_Contains_int(); - - AssertSql( - """ -@nullableInts={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" = ANY (@nullableInts) -""", - // - """ -@nullableInts={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."Int" = ANY (@nullableInts) AND p."Int" = ANY (@nullableInts) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int() - { - await base.Parameter_collection_of_nullable_ints_Contains_nullable_int(); - - AssertSql( - """ -@nullableInts={ 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableInt" = ANY (@nullableInts) OR (p."NullableInt" IS NULL AND array_position(@nullableInts, NULL) IS NOT NULL) -""", - // - """ -@nullableInts={ 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."NullableInt" = ANY (@nullableInts) AND p."NullableInt" = ANY (@nullableInts) IS NOT NULL) AND (p."NullableInt" IS NOT NULL OR array_position(@nullableInts, NULL) IS NULL) -"""); - } - - public override async Task Parameter_collection_of_structs_Contains_struct() - { - await base.Parameter_collection_of_structs_Contains_struct(); - - AssertSql( - """ -@values={ '22' -'33' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."WrappedId" = ANY (@values) -""", - // - """ -@values={ '11' -'44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."WrappedId" = ANY (@values) AND p."WrappedId" = ANY (@values) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_of_strings_Contains_string() - { - await base.Parameter_collection_of_strings_Contains_string(); - - AssertSql( - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."String" = ANY (@strings) -""", - // - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."String" = ANY (@strings) AND p."String" = ANY (@strings) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_of_strings_Contains_nullable_string() - { - await base.Parameter_collection_of_strings_Contains_nullable_string(); - - AssertSql( - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableString" = ANY (@strings) OR (p."NullableString" IS NULL AND array_position(@strings, NULL) IS NOT NULL) -""", - // - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."NullableString" = ANY (@strings) AND p."NullableString" = ANY (@strings) IS NOT NULL) AND (p."NullableString" IS NOT NULL OR array_position(@strings, NULL) IS NULL) -"""); - } - - public override async Task Parameter_collection_of_nullable_strings_Contains_string() - { - await base.Parameter_collection_of_nullable_strings_Contains_string(); - - AssertSql( - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."String" = ANY (@strings) -""", - // - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."String" = ANY (@strings) AND p."String" = ANY (@strings) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_of_nullable_strings_Contains_nullable_string() - { - await base.Parameter_collection_of_nullable_strings_Contains_nullable_string(); - - AssertSql( - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableString" = ANY (@strings) OR (p."NullableString" IS NULL AND array_position(@strings, NULL) IS NOT NULL) -""", - // - """ -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."NullableString" = ANY (@strings) AND p."NullableString" = ANY (@strings) IS NOT NULL) AND (p."NullableString" IS NOT NULL OR array_position(@strings, NULL) IS NULL) -"""); - } - - public override async Task Parameter_collection_of_DateTimes_Contains() - { - await base.Parameter_collection_of_DateTimes_Contains(); - - AssertSql( - """ -@dateTimes={ '2020-01-10T12:30:00.0000000Z' -'9999-01-01T00:00:00.0000000Z' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."DateTime" = ANY (@dateTimes) -"""); - } - - public override async Task Parameter_collection_of_bools_Contains() - { - await base.Parameter_collection_of_bools_Contains(); - - AssertSql( - """ -@bools={ 'True' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Bool" = ANY (@bools) -"""); - } - - public override async Task Parameter_collection_of_enums_Contains() - { - await base.Parameter_collection_of_enums_Contains(); - - AssertSql( - """ -@enums={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Enum" = ANY (@enums) -"""); - } - - public override async Task Parameter_collection_null_Contains() - { - await base.Parameter_collection_null_Contains(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Int" = ANY (NULL) -"""); - } - - public override async Task Parameter_collection_Contains_with_EF_Constant() - { - await base.Parameter_collection_Contains_with_EF_Constant(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" IN (2, 999, 1000) -"""); - } - - public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any() - { - await base.Parameter_collection_Where_with_EF_Constant_Where_Any(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE EXISTS ( - SELECT 1 - FROM (VALUES (2::int), (999), (1000)) AS i("Value") - WHERE i."Value" > 0) -"""); - } - - public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant() - { - await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (2::int), (999), (1000)) AS i("Value") - WHERE i."Value" > p."Id") = 2 -"""); - } - - // The following test does nothing since we don't override NumberOfValuesForHugeParameterCollectionTests; - // the PG parameter limit is huge (ushort), so we don't need to test it. - public override async Task Parameter_collection_Count_with_huge_number_of_values() - { - await base.Parameter_collection_Count_with_huge_number_of_values(); - - AssertSql(); - } - - // The following test does nothing since we don't override NumberOfValuesForHugeParameterCollectionTests; - // the PG parameter limit is huge (ushort), so we don't need to test it. - public override async Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values() - { - await base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values(); - - AssertSql(); - } - - [ConditionalFact] // #3012 - [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 - public virtual async Task Parameter_collection_of_ranges_Contains() - { - var ranges = new NpgsqlRange[] - { - new(5, 15), - new(40, 50) - }; - - await AssertQuery( - 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={ '[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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE @ranges @> p."Int" -"""); - } - - public override async Task Column_collection_of_ints_Contains() - { - await base.Column_collection_of_ints_Contains(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE 10 = ANY (p."Ints") -"""); - } - - public override async Task Column_collection_of_nullable_ints_Contains() - { - await base.Column_collection_of_nullable_ints_Contains(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE 10 = ANY (p."NullableInts") -"""); - } - - public override async Task Column_collection_of_nullable_ints_Contains_null() - { - await base.Column_collection_of_nullable_ints_Contains_null(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE array_position(p."NullableInts", NULL) IS NOT NULL -"""); - } - - public override async Task Column_collection_of_strings_contains_null() - { - await base.Column_collection_of_strings_contains_null(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE array_position(p."Strings", NULL) IS NOT NULL -"""); - } - - public override async Task Column_collection_of_nullable_strings_contains_null() - { - await base.Column_collection_of_nullable_strings_contains_null(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE array_position(p."NullableStrings", NULL) IS NOT NULL -"""); - } - - public override async Task Column_collection_of_bools_Contains() - { - await base.Column_collection_of_bools_Contains(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE TRUE = ANY (p."Bools") -"""); - } - - public override async Task Column_collection_Count_method() - { - await base.Column_collection_Count_method(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(p."Ints") = 2 -"""); - } - - public override async Task Column_collection_Length() - { - await base.Column_collection_Length(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(p."Ints") = 2 -"""); - } - - public override async Task Column_collection_Count_with_predicate() - { - await base.Column_collection_Count_with_predicate(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Where_Count(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_index_int(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints"[2] = 10 -"""); - } - - public override async Task Column_collection_index_string() - { - await base.Column_collection_index_string(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Strings"[2] = '10' -"""); - } - - public override async Task Column_collection_index_datetime() - { - await base.Column_collection_index_datetime(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."DateTimes"[2] = TIMESTAMPTZ '2020-01-10T12:30:00Z' -"""); - } - - public override async Task Column_collection_index_beyond_end() - { - await base.Column_collection_index_beyond_end(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints"[1000] = 10 -"""); - } - - public override async Task Nullable_reference_column_collection_index_equals_nullable_column() - { - await base.Nullable_reference_column_collection_index_equals_nullable_column(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableStrings"[3] = p."NullableString" OR (p."NullableStrings"[3] IS NULL AND p."NullableString" IS NULL) -"""); - } - - public override async Task Non_nullable_reference_column_collection_index_equals_nullable_column() - { - await base.Non_nullable_reference_column_collection_index_equals_nullable_column(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(p."Strings") > 0 AND p."Strings"[2] = p."NullableString" -"""); - } - - public override async Task Inline_collection_index_Column() - { - await base.Inline_collection_index_Column(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT v."Value" - FROM (VALUES (0, 1::int), (1, 2), (2, 3)) AS v(_ord, "Value") - ORDER BY v._ord NULLS FIRST - LIMIT 1 OFFSET p."Int") = 1 -"""); - } - - public override async Task Inline_collection_index_Column_with_EF_Constant() - { - await base.Inline_collection_index_Column_with_EF_Constant(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT i."Value" - FROM (VALUES (0, 1::int), (1, 2), (2, 3)) AS i(_ord, "Value") - ORDER BY i._ord NULLS FIRST - LIMIT 1 OFFSET p."Int") = 1 -"""); - } - - public override async Task Inline_collection_value_index_Column() - { - await base.Inline_collection_value_index_Column(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Inline_collection_List_value_index_Column(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Parameter_collection_index_Column_equal_Column(); - - AssertSql( - """ -@ints={ '0' -'2' -'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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE @ints[p."Int" + 1] = p."Int" -"""); - } - - public override async Task Parameter_collection_index_Column_equal_constant() - { - await base.Parameter_collection_index_Column_equal_constant(); - - AssertSql( - """ -@ints={ '1' -'2' -'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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE @ints[p."Int" + 1] = 1 -"""); - } - - public override async Task Column_collection_ElementAt() - { - await base.Column_collection_ElementAt(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints"[2] = 10 -"""); - } - - public override async Task Column_collection_First() - { - await base.Column_collection_First(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_FirstOrDefault(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Single(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_SingleOrDefault(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Skip(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(p."Ints"[2:]) = 2 -"""); - } - - public override async Task Column_collection_Take() - { - await base.Column_collection_Take(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE 11 = ANY (p."Ints"[:2]) -"""); - } - - public override async Task Column_collection_Skip_Take() - { - await base.Column_collection_Skip_Take(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE 11 = ANY (p."Ints"[2:3]) -"""); - } - - public override async Task Column_collection_Where_Skip() - { - await base.Column_collection_Where_Skip(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Where_Take(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Where_Skip_Take(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Contains_over_subquery(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE 11 IN ( - SELECT i.value - FROM unnest(p."Ints") AS i(value) - WHERE i.value > 1 -) -"""); - } - - public override async Task Column_collection_OrderByDescending_ElementAt() - { - await base.Column_collection_OrderByDescending_ElementAt(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT i.value - FROM unnest(p."Ints") AS i(value) - ORDER BY i.value DESC NULLS LAST - LIMIT 1 OFFSET 0) = 111 -"""); - } - - public override async Task Column_collection_Where_ElementAt() - { - await base.Column_collection_Where_ElementAt(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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() - { - await base.Column_collection_Any(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(p."Ints") > 0 -"""); - } - - public override async Task Column_collection_Distinct() - { - await base.Column_collection_Distinct(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT DISTINCT i.value - FROM unnest(p."Ints") AS i(value) - ) AS i0) = 3 -"""); - } - - public override async Task Column_collection_SelectMany() - { - await base.Column_collection_SelectMany(); - - 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() - { - await base.Column_collection_SelectMany_with_filter(); - - 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() - { - await base.Column_collection_SelectMany_with_Select_to_anonymous_type(); - - 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 -"""); - } - - public override async Task Column_collection_projection_from_top_level() - { - await base.Column_collection_projection_from_top_level(); - - AssertSql( - """ -SELECT p."Ints" -FROM "PrimitiveCollectionsEntity" AS p -ORDER BY p."Id" NULLS FIRST -"""); - } - - public override async Task Column_collection_Join_parameter_collection() - { - await base.Column_collection_Join_parameter_collection(); - - AssertSql( - """ -@ints={ '11' -'111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM unnest(p."Ints") AS i(value) - INNER JOIN unnest(@ints) AS i0(value) ON i.value = i0.value) = 2 -"""); - } - - public override async Task Inline_collection_Join_ordered_column_collection() - { - await base.Inline_collection_Join_ordered_column_collection(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (11::int), (111)) AS v("Value") - INNER JOIN unnest(p."Ints") AS i(value) ON v."Value" = i.value) = 2 -"""); - } - - public override async Task Parameter_collection_Concat_column_collection() - { - await base.Parameter_collection_Concat_column_collection(); - - AssertSql( - """ -@ints={ '11' -'111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(@ints || p."Ints") = 2 -"""); - } - - public override async Task Parameter_collection_with_type_inference_for_JsonScalarExpression() - { - await base.Parameter_collection_with_type_inference_for_JsonScalarExpression(); - - AssertSql( - """ -@values={ 'one' -'two' } (DbType = Object) - -SELECT CASE - WHEN p."Id" <> 0 THEN @values[p."Int" % 2 + 1] - ELSE 'foo' -END -FROM "PrimitiveCollectionsEntity" AS p -"""); - } - - public override async Task Column_collection_Union_parameter_collection() - { - await base.Column_collection_Union_parameter_collection(); - - AssertSql( - """ -@ints={ '11' -'111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT i.value - FROM unnest(p."Ints") AS i(value) - UNION - SELECT i0.value - FROM unnest(@ints) AS i0(value) - ) AS u) = 2 -"""); - } - - public override async Task Column_collection_Intersect_inline_collection() - { - await base.Column_collection_Intersect_inline_collection(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT i.value - FROM unnest(p."Ints") AS i(value) - INTERSECT - VALUES (11::int), (111) - ) AS i0) = 2 -"""); - } - - [ConditionalFact] - public virtual async Task Column_collection_Intersect_Parameter_collection_Any() - { - var ints = new[] { 11, 12 }; - - await AssertQuery(ss => ss.Set().Where(c => c.Ints.Intersect(ints).Any())); - - AssertSql( - """ -@ints={ '11' -'12' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints" && @ints -"""); - } - - public override async Task Inline_collection_Except_column_collection() - { - await base.Inline_collection_Except_column_collection(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT v."Value" - FROM (VALUES (11::int), (111)) AS v("Value") - EXCEPT - SELECT i.value AS "Value" - FROM unnest(p."Ints") AS i(value) - ) AS e - WHERE e."Value" % 2 = 1) = 2 -"""); - } - - public override async Task Column_collection_Where_Union() - { - await base.Column_collection_Where_Union(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -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 -"""); - } - - [ConditionalFact] - public virtual async Task Parameter_collection_Concat_Column_collection_Concat_parameter() - { - var ints1 = new[] { 11 }; - var ints2 = new[] { 12 }; - - await AssertQuery( - ss => ss.Set().Where(c => ints1.Concat(c.Ints).Concat(ints2).Count() == 4)); - - AssertSql( - """ -@ints1={ '11' } (DbType = Object) -@ints2={ '12' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(@ints1 || p."Ints" || @ints2) = 4 -"""); - } - - public override async Task Column_collection_Concat_parameter_collection_equality_inline_collection() - { - await base.Column_collection_Concat_parameter_collection_equality_inline_collection(); - - AssertSql(); - } - - public override async Task Column_collection_equality_parameter_collection() - { - await base.Column_collection_equality_parameter_collection(); - - AssertSql( - """ -@ints={ '1' -'10' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints" = @ints -"""); - } - - public override async Task Column_collection_equality_inline_collection() - { - await base.Column_collection_equality_inline_collection(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints" = ARRAY[1,10]::integer[] -"""); - } - - public override async Task Column_collection_equality_inline_collection_with_parameters() - { - var (i, j) = (1, 10); - - await AssertQuery( - ss => ss.Set().Where(c => c.Ints == new[] { i, j }), - ss => ss.Set().Where(c => c.Ints.SequenceEqual(new[] { i, j }))); - - AssertSql( - """ -@i='1' -@j='10' - -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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Ints" = ARRAY[@i,@j]::integer[] -"""); - } - - public override async Task Column_collection_Where_equality_inline_collection() - { - await base.Column_collection_Where_equality_inline_collection(); - - AssertSql(); - } - - public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query() - { - await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(); - - AssertSql( - """ -@ints={ '10' -'111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT i.value - FROM unnest(@ints[2:]) AS i(value) - UNION - SELECT i0.value - FROM unnest(p."Ints") AS i0(value) - ) AS u) = 3 -"""); - } - - public override async Task Parameter_collection_in_subquery_Union_column_collection() - { - await base.Parameter_collection_in_subquery_Union_column_collection(); - - AssertSql( - """ -@Skip={ '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT s.value - FROM unnest(@Skip) AS s(value) - UNION - SELECT i.value - FROM unnest(p."Ints") AS i(value) - ) AS u) = 3 -"""); - } - - public override async Task Parameter_collection_in_subquery_Union_column_collection_nested() - { - await base.Parameter_collection_in_subquery_Union_column_collection_nested(); - - AssertSql( - """ -@Skip={ '111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT s.value - FROM unnest(@Skip) AS s(value) - UNION - SELECT i2.value - FROM ( - SELECT i1.value - FROM ( - SELECT DISTINCT i0.value - FROM ( - SELECT i.value - FROM unnest(p."Ints") AS i(value) - ORDER BY i.value NULLS FIRST - OFFSET 1 - ) AS i0 - ) AS i1 - ORDER BY i1.value DESC NULLS LAST - LIMIT 20 - ) AS i2 - ) AS u) = 3 -"""); - } - - public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() - { - base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); - - AssertSql(); - } - - public override async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query() - { - await base.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(); - - AssertSql(); - } - - public override async Task Parameter_collection_in_subquery_Count_as_compiled_query() - { - await base.Parameter_collection_in_subquery_Count_as_compiled_query(); - - AssertSql( - """ -@ints={ '10' -'111' } (DbType = Object) - -SELECT count(*)::int -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM unnest(@ints[2:]) AS i(value) - WHERE i.value > p."Id") = 1 -"""); - } - - public override async Task Column_collection_in_subquery_Union_parameter_collection() - { - await base.Column_collection_in_subquery_Union_parameter_collection(); - - AssertSql( - """ -@ints={ '10' -'111' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM ( - SELECT i.value - FROM unnest(p."Ints"[2:]) AS i(value) - UNION - SELECT i0.value - FROM unnest(@ints) AS i0(value) - ) AS u) = 3 -"""); - } - - public override async Task Project_collection_of_ints_simple() - { - await base.Project_collection_of_ints_simple(); - - AssertSql( - """ -SELECT p."Ints" -FROM "PrimitiveCollectionsEntity" AS p -ORDER BY p."Id" NULLS FIRST -"""); - } - - public override async Task Project_collection_of_ints_ordered() - { - await base.Project_collection_of_ints_ordered(); - - AssertSql( - """ -SELECT p."Id", i.value, i.ordinality -FROM "PrimitiveCollectionsEntity" AS p -LEFT JOIN LATERAL unnest(p."Ints") WITH ORDINALITY AS i(value) ON TRUE -ORDER BY p."Id" NULLS FIRST, i.value DESC NULLS LAST -"""); - } - - public override async Task Project_collection_of_datetimes_filtered() - { - await base.Project_collection_of_datetimes_filtered(); - - AssertSql( - """ -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 -) AS d0 ON TRUE -ORDER BY p."Id" NULLS FIRST, d0.ordinality NULLS FIRST -"""); - } - - public override async Task Project_collection_of_nullable_ints_with_paging() - { - await base.Project_collection_of_nullable_ints_with_paging(); - - AssertSql( - """ -SELECT p."Id", n.value, n.ordinality -FROM "PrimitiveCollectionsEntity" AS p -LEFT JOIN LATERAL unnest(p."NullableInts"[:20]) WITH ORDINALITY AS n(value) ON TRUE -ORDER BY p."Id" NULLS FIRST -"""); - } - - public override async Task Project_collection_of_nullable_ints_with_paging2() - { - await base.Project_collection_of_nullable_ints_with_paging2(); - - AssertSql( - """ -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 n0 ON TRUE -ORDER BY p."Id" NULLS FIRST, n0.value NULLS FIRST -"""); - } - - public override async Task Project_collection_of_nullable_ints_with_paging3() - { - await base.Project_collection_of_nullable_ints_with_paging3(); - - AssertSql( - """ -SELECT p."Id", n.value, n.ordinality -FROM "PrimitiveCollectionsEntity" AS p -LEFT JOIN LATERAL unnest(p."NullableInts"[3:]) WITH ORDINALITY AS n(value) ON TRUE -ORDER BY p."Id" NULLS FIRST -"""); - } - - public override async Task Project_collection_of_ints_with_distinct() - { - await base.Project_collection_of_ints_with_distinct(); - - AssertSql( - """ -SELECT p."Id", i0.value -FROM "PrimitiveCollectionsEntity" AS p -LEFT JOIN LATERAL ( - SELECT DISTINCT i.value - FROM unnest(p."Ints") AS i(value) -) AS i0 ON TRUE -ORDER BY p."Id" NULLS FIRST -"""); - } - - public override async Task Project_collection_of_nullable_ints_with_distinct() - { - await base.Project_collection_of_nullable_ints_with_distinct(); - - AssertSql(); - } - - public override async Task Project_collection_of_ints_with_ToList_and_FirstOrDefault() - { - await base.Project_collection_of_ints_with_ToList_and_FirstOrDefault(); - - 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() - { - await base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(); - - AssertSql( - """ -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 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 n2 ON TRUE -ORDER BY p."Id" NULLS FIRST, n1.ordinality NULLS FIRST, n2.ordinality NULLS FIRST -"""); - } - - public override async Task Project_multiple_collections() - { - // Base implementation currently uses an Unspecified DateTime in the query, but we require a Utc one. - await AssertQuery( - ss => ss.Set().OrderBy(x => x.Id).Select( - x => new - { - Ints = x.Ints.ToList(), - OrderedInts = x.Ints.OrderByDescending(xx => xx).ToList(), - FilteredDateTimes = x.DateTimes.Where(xx => xx.Day != 1).ToList(), - FilteredDateTimes2 = x.DateTimes.Where(xx => xx > new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ToList() - }), - elementAsserter: (e, a) => - { - AssertCollection(e.Ints, a.Ints, ordered: true); - AssertCollection(e.OrderedInts, a.OrderedInts, ordered: true); - AssertCollection(e.FilteredDateTimes, a.FilteredDateTimes, elementSorter: ee => ee); - AssertCollection(e.FilteredDateTimes2, a.FilteredDateTimes2, elementSorter: ee => ee); - }, - assertOrder: true); - - AssertSql( - """ -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 -) 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 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 -"""); - } - - public override async Task Project_primitive_collections_element() - { - await base.Project_primitive_collections_element(); - - AssertSql( - """ -SELECT p."Ints"[1] AS "Indexer", p."DateTimes"[1] AS "EnumerableElementAt", p."Strings"[2] AS "QueryableElementAt" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."Id" < 4 -ORDER BY p."Id" NULLS FIRST -"""); - } - - public override async Task Project_inline_collection() - { - await base.Project_inline_collection(); - - AssertSql( - """ -SELECT ARRAY[p."String",'foo']::text[] -FROM "PrimitiveCollectionsEntity" AS p -"""); - } - - public override async Task Project_inline_collection_with_Union() - { - await base.Project_inline_collection_with_Union(); - - 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() - { - await base.Project_inline_collection_with_Concat(); - - AssertSql(); - } - - public override async Task Nested_contains_with_Lists_and_no_inferred_type_mapping() - { - await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(); - - AssertSql( - """ -@ints={ '1' -'2' -'3' } (DbType = Object) -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE CASE - WHEN p."Int" = ANY (@ints) THEN 'one' - ELSE 'two' -END = ANY (@strings) -"""); - } - - public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapping() - { - await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(); - - AssertSql( - """ -@ints={ '1' -'2' -'3' } (DbType = Object) -@strings={ '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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE CASE - WHEN p."Int" = ANY (@ints) THEN 'one' - ELSE 'two' -END = ANY (@strings) -"""); - } - - public override async Task Values_of_enum_casted_to_underlying_value() - { - await base.Values_of_enum_casted_to_underlying_value(); - - 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE ( - SELECT count(*)::int - FROM (VALUES (0::int), (1), (2), (3)) AS v("Value") - WHERE v."Value" = p."Int") > 0 -"""); - } - - [ConditionalFact] - public virtual async Task Array_remove() - { - await AssertQuery( - // 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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE cardinality(array_remove(p."Ints", 1)) = 1 -"""); - } - - public override async Task Parameter_collection_of_structs_Contains_nullable_struct() - { - await base.Parameter_collection_of_structs_Contains_nullable_struct(); - - AssertSql( - """ -@values={ '22' -'33' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableWrappedId" = ANY (@values) OR (p."NullableWrappedId" IS NULL AND array_position(@values, NULL) IS NOT NULL) -""", - // - """ -@values={ '11' -'44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."NullableWrappedId" = ANY (@values) AND p."NullableWrappedId" = ANY (@values) IS NOT NULL) AND (p."NullableWrappedId" IS NOT NULL OR array_position(@values, NULL) IS NULL) -"""); - } - - public override Task Parameter_collection_of_structs_Contains_nullable_struct_with_nullable_comparer() - => Assert.ThrowsAnyAsync( - () => base.Parameter_collection_of_structs_Contains_nullable_struct_with_nullable_comparer()); - - public override async Task Parameter_collection_of_nullable_structs_Contains_struct() - { - await base.Parameter_collection_of_nullable_structs_Contains_struct(); - - AssertSql( - """ -@values={ NULL -'22' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."WrappedId" = ANY (@values) -""", - // - """ -@values={ '11' -'44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."WrappedId" = ANY (@values) AND p."WrappedId" = ANY (@values) IS NOT NULL) -"""); - } - - public override async Task Parameter_collection_of_nullable_structs_Contains_nullable_struct() - { - await base.Parameter_collection_of_nullable_structs_Contains_nullable_struct(); - - AssertSql( - """ -@values={ NULL -'22' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE p."NullableWrappedId" = ANY (@values) OR (p."NullableWrappedId" IS NULL AND array_position(@values, NULL) IS NOT NULL) -""", - // - """ -@values={ '11' -'44' } (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."NullableWrappedId", p."NullableWrappedIdWithNullableComparer", p."String", p."Strings", p."WrappedId" -FROM "PrimitiveCollectionsEntity" AS p -WHERE NOT (p."NullableWrappedId" = ANY (@values) AND p."NullableWrappedId" = ANY (@values) IS NOT NULL) AND (p."NullableWrappedId" IS NOT NULL OR array_position(@values, NULL) IS NULL) -"""); - } - - public override Task Parameter_collection_of_nullable_structs_Contains_nullable_struct_with_nullable_comparer() - => Assert.ThrowsAnyAsync( - () => base.Parameter_collection_of_nullable_structs_Contains_nullable_struct_with_nullable_comparer()); - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - private PrimitiveCollectionsContext CreateContext() - => Fixture.CreateContext(); - - public class PrimitiveCollectionsQueryNpgsqlFixture : PrimitiveCollectionsQueryFixtureBase, ITestSqlLoggerFactory - { - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs deleted file mode 100644 index 6f9ee42db0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/QueryFilterFuncletizationNpgsqlTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class QueryFilterFuncletizationNpgsqlTest - : QueryFilterFuncletizationTestBase -{ - // ReSharper disable once UnusedParameter.Local - public QueryFilterFuncletizationNpgsqlTest( - QueryFilterFuncletizationNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public override void DbContext_list_is_parameterized() - { - using var context = CreateContext(); - - // The standard EF Core implementation expands the query filter-referenced list client side, and so generates an NRE - // when the list is null. We translate to server-side with PostgresAnyExpression, so no exception is thrown. - // Assert.Throws(() => context.Set().ToList()); - - context.TenantIds = []; - var query = context.Set().ToList(); - Assert.Empty(query); - - context.TenantIds = [1]; - query = context.Set().ToList(); - Assert.Single(query); - - context.TenantIds = [2, 3]; - query = context.Set().ToList(); - Assert.Equal(2, query.Count); - } - - public class QueryFilterFuncletizationNpgsqlFixture : QueryFilterFuncletizationRelationalFixture - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs deleted file mode 100644 index 709aac3036..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlFixture.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class QueryNoClientEvalNpgsqlFixture : NorthwindQueryNpgsqlFixture; diff --git a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs deleted file mode 100644 index de88a58336..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/QueryNoClientEvalNpgsqlTest.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class QueryNoClientEvalNpgsqlTest(QueryNoClientEvalNpgsqlFixture fixture) - : QueryNoClientEvalTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs deleted file mode 100644 index f217715c7a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/SharedTypeQueryNpgsqlTest.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class SharedTypeQueryNpgsqlTest(NonSharedFixture fixture) : SharedTypeQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - 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 -} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs deleted file mode 100644 index 6395b7b506..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlFixture.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class SpatialQueryNpgsqlFixture : SpatialQueryRelationalFixture -{ - // 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 - => new NpgsqlTestStoreFactory(useConnectionString: true); - - protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNetTopologySuite(); - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("postgis"); - } - - // TODO: #1232 - // protected override bool CanExecuteQueryString => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs deleted file mode 100644 index 3e3bd42b10..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs +++ /dev/null @@ -1,483 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; -using NetTopologySuite; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -// ReSharper disable once UnusedMember.Global -[RequiresPostgis] -public class SpatialQueryNpgsqlGeographyTest - : SpatialQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public SpatialQueryNpgsqlGeographyTest(SpatialQueryNpgsqlGeographyFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - protected override bool AssertDistances - => false; - - public static IEnumerable IsAsyncDataAndUseSpheroid = - [ - [false, false], [false, true], [true, false], [true, true] - ]; - - public override async Task Area(bool async) - { - await base.Area(async); - - AssertSql( - """ -SELECT p."Id", ST_Area(p."Polygon") AS "Area" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task AsBinary(bool async) - { - await base.AsBinary(async); - - AssertSql( - """ -SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" -FROM "PointEntity" AS p -"""); - } - - public override async Task AsText(bool async) - { - await base.AsText(async); - - AssertSql( - """ -SELECT p."Id", ST_AsText(p."Point") AS "Text" -FROM "PointEntity" AS p -"""); - } - - public override async Task Buffer(bool async) - { - await base.Buffer(async); - - AssertSql( - """ -SELECT p."Id", ST_Buffer(p."Polygon", 1.0) AS "Buffer" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Buffer_quadrantSegments(bool async) - { - await base.Buffer_quadrantSegments(async); - - AssertSql( - """ -SELECT p."Id", ST_Buffer(p."Polygon", 1.0, 8) AS "Buffer" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Centroid(bool async) - { - await base.Centroid(async); - - AssertSql( - """ -SELECT p."Id", ST_Centroid(p."Polygon") AS "Centroid" -FROM "PolygonEntity" AS p -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncDataAndUseSpheroid))] - public async Task Distance_with_spheroid(bool async, bool useSpheroid) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); - - await AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Distance = (double?)EF.Functions.Distance(e.Point, point, useSpheroid) }), - ss => ss.Set() - .Select(e => new { e.Id, Distance = (e.Point == null ? (double?)null : e.Point.Distance(point)) }), - elementSorter: e => e.Id, - elementAsserter: (e, a) => - { - Assert.Equal(e.Id, a.Id); - Assert.Equal(e.Distance is null, a.Distance is null); - }); - - AssertSql( - $""" -@point='POINT (0 1)' (DbType = Object) -@useSpheroid='{useSpheroid}' - -SELECT p."Id", ST_Distance(p."Point", @point, @useSpheroid) AS "Distance" -FROM "PointEntity" AS p -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DistanceKnn(bool async) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); - - await AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Distance = (double?)EF.Functions.DistanceKnn(e.Point, point) }), - ss => ss.Set() - .Select(e => new { e.Id, Distance = (e.Point == null ? (double?)null : e.Point.Distance(point)) }), - elementSorter: e => e.Id, - elementAsserter: (e, a) => - { - Assert.Equal(e.Id, a.Id); - Assert.Equal(e.Distance is null, a.Distance is null); - }); - - AssertSql( - """ -@point='POINT (0 1)' (DbType = Object) - -SELECT p."Id", p."Point" <-> @point AS "Distance" -FROM "PointEntity" AS p -"""); - } - - public override async Task GeometryType(bool async) - { - // PostGIS returns "POINT", NTS returns "Point" - await AssertQuery( - async, - es => es.Set().Select( - e => new { e.Id, GeometryType = e.Point == null ? null : e.Point.GeometryType.ToLower() }), - x => x.Id); - - AssertSql( - """ -SELECT p."Id", CASE - WHEN p."Point" IS NULL THEN NULL - ELSE lower(GeometryType(p."Point")) -END AS "GeometryType" -FROM "PointEntity" AS p -"""); - } - - public override async Task Intersection(bool async) - { - await base.Intersection(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_Intersection(p."Polygon", @polygon) AS "Intersection" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Intersects(bool async) - { - await base.Intersects(async); - - AssertSql( - """ -@lineString='LINESTRING (0.5 -0.5, 0.5 0.5)' (DbType = Object) - -SELECT l."Id", ST_Intersects(l."LineString", @lineString) AS "Intersects" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task IsWithinDistance(bool async) - { - await base.IsWithinDistance(async); - - AssertSql( - """ -@point='POINT (0 1)' (DbType = Object) - -SELECT p."Id", ST_DWithin(p."Point", @point, 1.0) AS "IsWithinDistance" -FROM "PointEntity" AS p -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncDataAndUseSpheroid))] - public async Task IsWithinDistance_with_spheroid(bool async, bool useSpheroid) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); - - await AssertQuery( - async, - ss => ss.Set().Select( - e => new { e.Id, IsWithinDistance = (bool?)EF.Functions.IsWithinDistance(e.Point, point, 1, useSpheroid) }), - ss => ss.Set().Select( - e => new { e.Id, IsWithinDistance = e.Point == null ? (bool?)null : e.Point.IsWithinDistance(point, 1) }), - elementSorter: e => e.Id, - elementAsserter: (e, a) => - { - Assert.Equal(e.Id, a.Id); - - if (e.IsWithinDistance is null) - { - Assert.False(a.IsWithinDistance ?? false); - } - else - { - Assert.NotNull(a.IsWithinDistance); - } - }); - - AssertSql( - $""" -@point='POINT (0 1)' (DbType = Object) -@useSpheroid='{useSpheroid}' - -SELECT p."Id", ST_DWithin(p."Point", @point, 1.0, @useSpheroid) AS "IsWithinDistance" -FROM "PointEntity" AS p -"""); - } - - public override async Task Length(bool async) - { - await base.Length(async); - - AssertSql( - """ -SELECT l."Id", ST_Length(l."LineString") AS "Length" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task SRID(bool async) - { - await base.SRID(async); - - AssertSql( - """ -SELECT p."Id", ST_SRID(p."Point") AS "SRID" -FROM "PointEntity" AS p -"""); - } - - public override async Task ToBinary(bool async) - { - await base.ToBinary(async); - - AssertSql( - """ -SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" -FROM "PointEntity" AS p -"""); - } - - public override async Task ToText(bool async) - { - await base.ToText(async); - - AssertSql( - """ -SELECT p."Id", ST_AsText(p."Point") AS "Text" -FROM "PointEntity" AS p -"""); - } - - #region Not supported on geography - - public override Task Boundary(bool async) - => Task.CompletedTask; - - public override Task Combine_aggregate(bool async) - => Task.CompletedTask; - - public override Task EnvelopeCombine_aggregate(bool async) - => Task.CompletedTask; - - public override Task Contains(bool async) - => Task.CompletedTask; - - public override Task ConvexHull(bool async) - => Task.CompletedTask; - - public override Task ConvexHull_aggregate(bool async) - => Task.CompletedTask; - - public override Task Crosses(bool async) - => Task.CompletedTask; - - public override Task Difference(bool async) - => Task.CompletedTask; - - public override Task Dimension(bool async) - => Task.CompletedTask; - - public override Task Disjoint_with_cast_to_nullable(bool async) - => Task.CompletedTask; - - public override Task Disjoint_with_null_check(bool async) - => Task.CompletedTask; - - public override Task EndPoint(bool async) - => Task.CompletedTask; - - public override Task Envelope(bool async) - => Task.CompletedTask; - - public override Task EqualsTopologically(bool async) - => Task.CompletedTask; - - public override Task ExteriorRing(bool async) - => Task.CompletedTask; - - public override Task GetGeometryN(bool async) - => Task.CompletedTask; - - public override Task GetGeometryN_with_null_argument(bool async) - => Task.CompletedTask; - - public override Task GetInteriorRingN(bool async) - => Task.CompletedTask; - - public override Task GetPointN(bool async) - => Task.CompletedTask; - - public override Task ICurve_IsClosed(bool async) - => Task.CompletedTask; - - public override Task IGeometryCollection_Count(bool async) - => Task.CompletedTask; - - public override Task IMultiCurve_IsClosed(bool async) - => Task.CompletedTask; - - public override Task IsEmpty(bool async) - => Task.CompletedTask; - - public override Task IsEmpty_equal_to_null(bool async) - => Task.CompletedTask; - - public override Task IsEmpty_not_equal_to_null(bool async) - => Task.CompletedTask; - - public override Task IsRing(bool async) - => Task.CompletedTask; - - public override Task IsSimple(bool async) - => Task.CompletedTask; - - public override Task IsValid(bool async) - => Task.CompletedTask; - - public override Task Item(bool async) - => Task.CompletedTask; - - public override Task InteriorPoint(bool async) - => Task.CompletedTask; - - public override Task LineString_Count(bool async) - => Task.CompletedTask; - - public override Task M(bool async) - => Task.CompletedTask; - - public override Task Normalized(bool async) - => Task.CompletedTask; - - public override Task NumGeometries(bool async) - => Task.CompletedTask; - - public override Task NumInteriorRings(bool async) - => Task.CompletedTask; - - public override Task NumPoints(bool async) - => Task.CompletedTask; - - public override Task OgcGeometryType(bool async) - => Task.CompletedTask; - - public override Task Overlaps(bool async) - => Task.CompletedTask; - - public override Task PointOnSurface(bool async) - => Task.CompletedTask; - - public override Task Relate(bool async) - => Task.CompletedTask; - - public override Task Reverse(bool async) - => Task.CompletedTask; - - public override Task StartPoint(bool async) - => Task.CompletedTask; - - public override Task SymmetricDifference(bool async) - => Task.CompletedTask; - - public override Task Touches(bool async) - => Task.CompletedTask; - - public override Task Union(bool async) - => Task.CompletedTask; - - public override Task Union_aggregate(bool async) - => Task.CompletedTask; - - public override Task Union_void(bool async) - => Task.CompletedTask; - - public override Task Within(bool async) - => Task.CompletedTask; - - public override Task X(bool async) - => Task.CompletedTask; - - public override Task Y(bool async) - => Task.CompletedTask; - - public override Task XY_with_collection_join(bool async) - => Task.CompletedTask; - - public override Task Z(bool async) - => Task.CompletedTask; - - #endregion - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class SpatialQueryNpgsqlGeographyFixture : SpatialQueryNpgsqlFixture - { - protected override string StoreName - => "SpatialQueryGeographyTest"; - - private NtsGeometryServices _geometryServices; - private GeometryFactory _geometryFactory; - - public NtsGeometryServices GeometryServices - => LazyInitializer.EnsureInitialized( - ref _geometryServices, - () => new NtsGeometryServices( - NtsGeometryServices.Instance.DefaultCoordinateSequenceFactory, - NtsGeometryServices.Instance.DefaultPrecisionModel, - 4326)); - - public override GeometryFactory GeometryFactory - => LazyInitializer.EnsureInitialized( - ref _geometryFactory, - () => GeometryServices.CreateGeometryFactory()); - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(null, null, Ordinates.None, true); - - return optionsBuilder; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs deleted file mode 100644 index 8f3eb1d612..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs +++ /dev/null @@ -1,782 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -[RequiresPostgis] -public class SpatialQueryNpgsqlGeometryTest - : SpatialQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public SpatialQueryNpgsqlGeometryTest(SpatialQueryNpgsqlGeometryFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Area(bool async) - { - await base.Area(async); - - AssertSql( - """ -SELECT p."Id", ST_Area(p."Polygon") AS "Area" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task AsBinary(bool async) - { - await base.AsBinary(async); - - AssertSql( - """ -SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" -FROM "PointEntity" AS p -"""); - } - - public override async Task AsText(bool async) - { - await base.AsText(async); - - AssertSql( - """ -SELECT p."Id", ST_AsText(p."Point") AS "Text" -FROM "PointEntity" AS p -"""); - } - - public override async Task Boundary(bool async) - { - await base.Boundary(async); - - AssertSql( - """ -SELECT p."Id", ST_Boundary(p."Polygon") AS "Boundary" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Buffer(bool async) - { - await base.Buffer(async); - - AssertSql( - """ -SELECT p."Id", ST_Buffer(p."Polygon", 1.0) AS "Buffer" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Buffer_quadrantSegments(bool async) - { - await base.Buffer_quadrantSegments(async); - - AssertSql( - """ -SELECT p."Id", ST_Buffer(p."Polygon", 1.0, 8) AS "Buffer" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Centroid(bool async) - { - await base.Centroid(async); - - AssertSql( - """ -SELECT p."Id", ST_Centroid(p."Polygon") AS "Centroid" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Combine_aggregate(bool async) - { - await base.Combine_aggregate(async); - - AssertSql( - """ -SELECT p."Group" AS "Id", ST_Collect(p."Point") AS "Combined" -FROM "PointEntity" AS p -WHERE p."Point" IS NOT NULL -GROUP BY p."Group" -"""); - } - - public override async Task EnvelopeCombine_aggregate(bool async) - { - await base.EnvelopeCombine_aggregate(async); - - AssertSql( - """ -SELECT p."Group" AS "Id", ST_Extent(p."Point")::geometry AS "Combined" -FROM "PointEntity" AS p -WHERE p."Point" IS NOT NULL -GROUP BY p."Group" -"""); - } - - public override async Task Contains(bool async) - { - await base.Contains(async); - - AssertSql( - """ -@point='POINT (0.25 0.25)' (DbType = Object) - -SELECT p."Id", ST_Contains(p."Polygon", @point) AS "Contains" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task ConvexHull(bool async) - { - await base.ConvexHull(async); - - AssertSql( - """ -SELECT p."Id", ST_ConvexHull(p."Polygon") AS "ConvexHull" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task ConvexHull_aggregate(bool async) - { - await base.ConvexHull_aggregate(async); - - AssertSql( - """ -SELECT p."Group" AS "Id", ST_ConvexHull(ST_Collect(p."Point")) AS "ConvexHull" -FROM "PointEntity" AS p -WHERE p."Point" IS NOT NULL -GROUP BY p."Group" -"""); - } - - public override async Task IGeometryCollection_Count(bool async) - { - await base.IGeometryCollection_Count(async); - - AssertSql( - """ -SELECT m."Id", ST_NumGeometries(m."MultiLineString") AS "Count" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task LineString_Count(bool async) - { - await base.LineString_Count(async); - - AssertSql( - """ -SELECT l."Id", ST_NumPoints(l."LineString") AS "Count" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task Crosses(bool async) - { - await base.Crosses(async); - - AssertSql( - """ -@lineString='LINESTRING (0.5 -0.5, 0.5 0.5)' (DbType = Object) - -SELECT l."Id", ST_Crosses(l."LineString", @lineString) AS "Crosses" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task Difference(bool async) - { - await base.Difference(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_Difference(p."Polygon", @polygon) AS "Difference" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Dimension(bool async) - { - await base.Dimension(async); - - AssertSql( - """ -SELECT p."Id", ST_Dimension(p."Point") AS "Dimension" -FROM "PointEntity" AS p -"""); - } - - // PostGIS refuses to operate on points of mixed SRIDs - public override Task Distance_constant_srid_4326(bool async) - => Task.CompletedTask; - - public override async Task EndPoint(bool async) - { - await base.EndPoint(async); - - AssertSql( - """ -SELECT l."Id", ST_EndPoint(l."LineString") AS "EndPoint" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task Envelope(bool async) - { - await base.Envelope(async); - - AssertSql( - """ -SELECT p."Id", ST_Envelope(p."Polygon") AS "Envelope" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task EqualsTopologically(bool async) - { - await base.EqualsTopologically(async); - - AssertSql( - """ -@point='POINT (0 0)' (DbType = Object) - -SELECT p."Id", ST_Equals(p."Point", @point) AS "EqualsTopologically" -FROM "PointEntity" AS p -"""); - } - - public override async Task ExteriorRing(bool async) - { - await base.ExteriorRing(async); - - AssertSql( - """ -SELECT p."Id", ST_ExteriorRing(p."Polygon") AS "ExteriorRing" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task GeometryType(bool async) - { - // PostGIS returns "POINT", NTS returns "Point" - await AssertQuery( - async, - es => es.Set().Select( - e => new { e.Id, GeometryType = e.Point == null ? null : e.Point.GeometryType.ToLower() }), - x => x.Id); - - AssertSql( - """ -SELECT p."Id", CASE - WHEN p."Point" IS NULL THEN NULL - ELSE lower(GeometryType(p."Point")) -END AS "GeometryType" -FROM "PointEntity" AS p -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Force2D(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Force2D = EF.Functions.Force2D(e.PointZ) }), - ss => ss.Set().Select(e => new { e.Id, Force2D = e.PointZ == null ? null : new Point(e.PointZ.X, e.PointZ.Y) }), - x => x.Id); - - AssertSql( - """ -SELECT p."Id", ST_Force2D(p."PointZ") AS "Force2D" -FROM "PointEntity" AS p -"""); - } - - public override async Task GetGeometryN(bool async) - { - await base.GetGeometryN(async); - - AssertSql( - """ -SELECT m."Id", ST_GeometryN(m."MultiLineString", 1) AS "Geometry0" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task GetInteriorRingN(bool async) - { - await base.GetInteriorRingN(async); - - AssertSql( - """ -SELECT p."Id", CASE - WHEN ST_NumInteriorRings(p."Polygon") = 0 THEN NULL - ELSE ST_InteriorRingN(p."Polygon", 1) -END AS "InteriorRing0" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task GetPointN(bool async) - { - await base.GetPointN(async); - - AssertSql( - """ -SELECT l."Id", ST_PointN(l."LineString", 1) AS "Point0" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task InteriorPoint(bool async) - { - await base.InteriorPoint(async); - - AssertSql( - """ -SELECT p."Id", ST_PointOnSurface(p."Polygon") AS "InteriorPoint", p."Polygon" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Intersection(bool async) - { - await base.Intersection(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_Intersection(p."Polygon", @polygon) AS "Intersection" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Intersects(bool async) - { - await base.Intersects(async); - - AssertSql( - """ -@lineString='LINESTRING (0.5 -0.5, 0.5 0.5)' (DbType = Object) - -SELECT l."Id", ST_Intersects(l."LineString", @lineString) AS "Intersects" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task ICurve_IsClosed(bool async) - { - await base.ICurve_IsClosed(async); - - AssertSql( - """ -SELECT l."Id", ST_IsClosed(l."LineString") AS "IsClosed" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task IMultiCurve_IsClosed(bool async) - { - await base.IMultiCurve_IsClosed(async); - - AssertSql( - """ -SELECT m."Id", ST_IsClosed(m."MultiLineString") AS "IsClosed" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task IsEmpty(bool async) - { - await base.IsEmpty(async); - - AssertSql( - """ -SELECT m."Id", ST_IsEmpty(m."MultiLineString") AS "IsEmpty" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task IsRing(bool async) - { - await base.IsRing(async); - - AssertSql( - """ -SELECT l."Id", ST_IsRing(l."LineString") AS "IsRing" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task IsSimple(bool async) - { - await base.IsSimple(async); - - AssertSql( - """ -SELECT l."Id", ST_IsSimple(l."LineString") AS "IsSimple" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task IsValid(bool async) - { - await base.IsValid(async); - - AssertSql( - """ -SELECT p."Id", ST_IsValid(p."Point") AS "IsValid" -FROM "PointEntity" AS p -"""); - } - - public override async Task IsWithinDistance(bool async) - { - await base.IsWithinDistance(async); - - AssertSql( - """ -@point='POINT (0 1)' (DbType = Object) - -SELECT p."Id", ST_DWithin(p."Point", @point, 1.0) AS "IsWithinDistance" -FROM "PointEntity" AS p -"""); - } - - public override async Task Item(bool async) - { - await base.Item(async); - - AssertSql( - """ -SELECT m."Id", ST_GeometryN(m."MultiLineString", 1) AS "Item0" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task Length(bool async) - { - await base.Length(async); - - AssertSql( - """ -SELECT l."Id", ST_Length(l."LineString") AS "Length" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task M(bool async) - { - await base.M(async); - - AssertSql( - """ -SELECT p."Id", ST_M(p."Point") AS "M" -FROM "PointEntity" AS p -"""); - } - - public override async Task NumGeometries(bool async) - { - await base.NumGeometries(async); - - AssertSql( - """ -SELECT m."Id", ST_NumGeometries(m."MultiLineString") AS "NumGeometries" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task NumInteriorRings(bool async) - { - await base.NumInteriorRings(async); - - AssertSql( - """ -SELECT p."Id", ST_NumInteriorRings(p."Polygon") AS "NumInteriorRings" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task NumPoints(bool async) - { - await base.NumPoints(async); - - AssertSql( - """ -SELECT l."Id", ST_NumPoints(l."LineString") AS "NumPoints" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task OgcGeometryType(bool async) - { - await base.OgcGeometryType(async); - - AssertSql( - """ -SELECT p."Id", CASE ST_GeometryType(p."Point") - WHEN 'ST_CircularString' THEN 8 - WHEN 'ST_CompoundCurve' THEN 9 - WHEN 'ST_CurvePolygon' THEN 10 - WHEN 'ST_GeometryCollection' THEN 7 - WHEN 'ST_LineString' THEN 2 - WHEN 'ST_MultiCurve' THEN 11 - WHEN 'ST_MultiLineString' THEN 5 - WHEN 'ST_MultiPoint' THEN 4 - WHEN 'ST_MultiPolygon' THEN 6 - WHEN 'ST_MultiSurface' THEN 12 - WHEN 'ST_Point' THEN 1 - WHEN 'ST_Polygon' THEN 3 - WHEN 'ST_PolyhedralSurface' THEN 15 - WHEN 'ST_Tin' THEN 16 -END AS "OgcGeometryType" -FROM "PointEntity" AS p -"""); - } - - public override async Task Overlaps(bool async) - { - await base.Overlaps(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_Overlaps(p."Polygon", @polygon) AS "Overlaps" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task PointOnSurface(bool async) - { - await base.PointOnSurface(async); - - AssertSql( - """ -SELECT p."Id", ST_PointOnSurface(p."Polygon") AS "PointOnSurface", p."Polygon" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Relate(bool async) - { - await base.Relate(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_Relate(p."Polygon", @polygon, '212111212') AS "Relate" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task SRID(bool async) - { - await base.SRID(async); - - AssertSql( - """ -SELECT p."Id", ST_SRID(p."Point") AS "SRID" -FROM "PointEntity" AS p -"""); - } - - public override async Task StartPoint(bool async) - { - await base.StartPoint(async); - - AssertSql( - """ -SELECT l."Id", ST_StartPoint(l."LineString") AS "StartPoint" -FROM "LineStringEntity" AS l -"""); - } - - public override async Task SymmetricDifference(bool async) - { - await base.SymmetricDifference(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_SymDifference(p."Polygon", @polygon) AS "SymmetricDifference" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task ToBinary(bool async) - { - await base.ToBinary(async); - - AssertSql( - """ -SELECT p."Id", ST_AsBinary(p."Point") AS "Binary" -FROM "PointEntity" AS p -"""); - } - - public override async Task ToText(bool async) - { - await base.ToText(async); - - AssertSql( - """ -SELECT p."Id", ST_AsText(p."Point") AS "Text" -FROM "PointEntity" AS p -"""); - } - - public override async Task Touches(bool async) - { - await base.Touches(async); - - AssertSql( - """ -@polygon='POLYGON ((0 1, 1 0, 1 1, 0 1))' (DbType = Object) - -SELECT p."Id", ST_Touches(p."Polygon", @polygon) AS "Touches" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Union(bool async) - { - await base.Union(async); - - AssertSql( - """ -@polygon='POLYGON ((0 0, 1 0, 1 1, 0 0))' (DbType = Object) - -SELECT p."Id", ST_Union(p."Polygon", @polygon) AS "Union" -FROM "PolygonEntity" AS p -"""); - } - - public override async Task Union_aggregate(bool async) - { - await base.Union_aggregate(async); - - AssertSql( - """ -SELECT p."Group" AS "Id", ST_Union(p."Point") AS "Union" -FROM "PointEntity" AS p -WHERE p."Point" IS NOT NULL -GROUP BY p."Group" -"""); - } - - public override async Task Union_void(bool async) - { - await base.Union_void(async); - - AssertSql( - """ -SELECT m."Id", ST_UnaryUnion(m."MultiLineString") AS "Union" -FROM "MultiLineStringEntity" AS m -"""); - } - - public override async Task Within(bool async) - { - await base.Within(async); - - AssertSql( - """ -@polygon='POLYGON ((-1 -1, 2 -1, 2 2, -1 2, -1 -1))' (DbType = Object) - -SELECT p."Id", ST_Within(p."Point", @polygon) AS "Within" -FROM "PointEntity" AS p -"""); - } - - public override async Task X(bool async) - { - await base.X(async); - - AssertSql( - """ -SELECT p."Id", ST_X(p."Point") AS "X" -FROM "PointEntity" AS p -"""); - } - - public override async Task Y(bool async) - { - await base.Y(async); - - AssertSql( - """ -SELECT p."Id", ST_Y(p."Point") AS "Y" -FROM "PointEntity" AS p -"""); - } - - public override async Task Z(bool async) - { - await base.Z(async); - - AssertSql( - """ -SELECT p."Id", ST_Z(p."Point") AS "Z" -FROM "PointEntity" AS p -"""); - } - - [ConditionalTheory(Skip = "#2850")] - [MemberData(nameof(IsAsyncData))] - public virtual async Task MultiString_Any(bool async) - { - 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 - // collection. - await AssertQuery( - async, - ss => ss.Set().Where(e => e.MultiLineString.Any(ls => ls == lineString)), - ss => ss.Set().Where( - e => e.MultiLineString != null && e.MultiLineString.Any(ls => GeometryComparer.Instance.Equals(ls, lineString))), - elementSorter: e => e.Id); - - AssertSql( - """ -@__lineString_0='LINESTRING (1 0, 1 1)' (DbType = Object) - -SELECT m."Id", m."MultiLineString" -FROM "MultiLineStringEntity" AS m -WHERE @__lineString_0 IN ( - SELECT m0.geom - FROM ST_Dump(m."MultiLineString") AS m0 -) -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class SpatialQueryNpgsqlGeometryFixture : SpatialQueryNpgsqlFixture - { - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(); - - return optionsBuilder; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs deleted file mode 100644 index ae750d7819..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/SqlExecutorNpgsqlTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Data.Common; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class SqlExecutorNpgsqlTest : SqlExecutorTestBase> -{ - 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"()"""; - - protected override string CustomerOrderHistorySproc - => """SELECT * FROM "CustOrderHist"(@CustomerID)"""; - - protected override string CustomerOrderHistoryWithGeneratedParameterSproc - => """SELECT * FROM "CustOrderHist"({0})"""; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs deleted file mode 100644 index 06d397871b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/SqlQueryNpgsqlTest.cs +++ /dev/null @@ -1,689 +0,0 @@ -using System.Data.Common; -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class SqlQueryNpgsqlTest : SqlQueryTestBase> -{ - public SqlQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task SqlQueryRaw_queryable_simple(bool async) - { - await base.SqlQueryRaw_queryable_simple(async); - - AssertSql( - """ -SELECT * FROM "Customers" WHERE "ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order(bool async) - { - await base.SqlQueryRaw_queryable_simple_columns_out_of_order(async); - - AssertSql( - """ -SELECT "Region", "PostalCode", "Phone", "Fax", "CustomerID", "Country", "ContactTitle", "ContactName", "CompanyName", "City", "Address" FROM "Customers" -"""); - } - - public override async Task SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(bool async) - { - await base.SqlQueryRaw_queryable_simple_columns_out_of_order_and_extra_columns(async); - - AssertSql( - """ -SELECT "Region", "PostalCode", "PostalCode" AS "Foo", "Phone", "Fax", "CustomerID", "Country", "ContactTitle", "ContactName", "CompanyName", "City", "Address" FROM "Customers" -"""); - } - - // 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); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_queryable_composed_after_removing_whitespaces(bool async) - { - await base.SqlQueryRaw_queryable_composed_after_removing_whitespaces(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - - -""" + " " + """ - - - - SELECT - * FROM "Customers" -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_queryable_composed_compiled(bool async) - { - await base.SqlQueryRaw_queryable_composed_compiled(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_queryable_composed_compiled_with_DbParameter(bool async) - { - await base.SqlQueryRaw_queryable_composed_compiled_with_DbParameter(async); - - AssertSql( - """ -customer='CONSH' (Nullable = false) - -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" WHERE "CustomerID" = @customer -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(bool async) - { - await base.SqlQueryRaw_queryable_composed_compiled_with_nameless_DbParameter(async); - - AssertSql( - """ -p0='CONSH' (Nullable = false) - -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" WHERE "CustomerID" = @p0 -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_queryable_composed_compiled_with_parameter(bool async) - { - await base.SqlQueryRaw_queryable_composed_compiled_with_parameter(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" WHERE "CustomerID" = 'CONSH' -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - - public override async Task SqlQueryRaw_composed_contains(bool async) - { - await base.SqlQueryRaw_composed_contains(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" -) AS m -WHERE m."CustomerID" IN ( - SELECT m0."CustomerID" - FROM ( - SELECT * FROM "Orders" - ) AS m0 -) -"""); - } - - public override async Task SqlQueryRaw_queryable_multiple_composed(bool async) - { - await base.SqlQueryRaw_queryable_multiple_composed(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode", m0."CustomerID", m0."EmployeeID", m0."Freight", m0."OrderDate", m0."OrderID", m0."RequiredDate", m0."ShipAddress", m0."ShipCity", m0."ShipCountry", m0."ShipName", m0."ShipPostalCode", m0."ShipRegion", m0."ShipVia", m0."ShippedDate" -FROM ( - SELECT * FROM "Customers" -) AS m -CROSS JOIN ( - SELECT * FROM "Orders" -) AS m0 -WHERE m."CustomerID" = m0."CustomerID" -"""); - } - - public override Task SqlQueryRaw_queryable_multiple_composed_with_closure_parameters(bool async) - // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but Npgsql rejects it because timestamptz - => Task.CompletedTask; - - public override Task SqlQueryRaw_queryable_multiple_composed_with_parameters_and_closure_parameters(bool async) - // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but Npgsql rejects it because timestamptz - => Task.CompletedTask; - - public override async Task SqlQueryRaw_queryable_multiple_line_query(bool async) - { - await base.SqlQueryRaw_queryable_multiple_line_query(async); - - AssertSql( - """ -SELECT * -FROM "Customers" -WHERE "City" = 'London' -"""); - } - - public override async Task SqlQueryRaw_queryable_composed_multiple_line_query(bool async) - { - await base.SqlQueryRaw_queryable_composed_multiple_line_query(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * - FROM "Customers" -) AS m -WHERE m."City" = 'London' -"""); - } - - public override async Task SqlQueryRaw_queryable_with_parameters(bool async) - { - await base.SqlQueryRaw_queryable_with_parameters(async); - - AssertSql( - """ -p0='London' -p1='Sales Representative' - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 -"""); - } - - public override async Task SqlQueryRaw_queryable_with_parameters_inline(bool async) - { - await base.SqlQueryRaw_queryable_with_parameters_inline(async); - - AssertSql( - """ -p0='London' -p1='Sales Representative' - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 -"""); - } - - public override async Task SqlQuery_queryable_with_parameters_interpolated(bool async) - { - await base.SqlQuery_queryable_with_parameters_interpolated(async); - - AssertSql( - """ -p0='London' -p1='Sales Representative' - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 -"""); - } - - public override async Task SqlQuery_queryable_with_parameters_inline_interpolated(bool async) - { - await base.SqlQuery_queryable_with_parameters_inline_interpolated(async); - - AssertSql( - """ -p0='London' -p1='Sales Representative' - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 -"""); - } - - public override Task SqlQuery_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated(bool async) - // Base implementation sends DateTime with Kind=Unspecified in a SQL query, but Npgsql rejects it because timestamptz - => Task.CompletedTask; - - public override async Task SqlQueryRaw_queryable_with_null_parameter(bool async) - { - await base.SqlQueryRaw_queryable_with_null_parameter(async); - - AssertSql( - """ -p0=NULL (Nullable = false) (DbType = Object) - -SELECT * FROM "Employees" WHERE "ReportsTo" = @p0 OR ("ReportsTo" IS NULL AND @p0 IS NULL) -"""); - } - - public override async Task SqlQueryRaw_queryable_with_parameters_and_closure(bool async) - { - var queryString = await base.SqlQueryRaw_queryable_with_parameters_and_closure(async); - - AssertSql( - """ -p0='London' -@contactTitle='Sales Representative' - -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" WHERE "City" = @p0 -) AS m -WHERE m."ContactTitle" = @contactTitle -"""); - - return null; - } - - public override async Task SqlQueryRaw_queryable_simple_cache_key_includes_query_string(bool async) - { - await base.SqlQueryRaw_queryable_simple_cache_key_includes_query_string(async); - - AssertSql( - """ -SELECT * FROM "Customers" WHERE "City" = 'London' -""", - // - """ -SELECT * FROM "Customers" WHERE "City" = 'Seattle' -"""); - } - - public override async Task SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(bool async) - { - await base.SqlQueryRaw_queryable_with_parameters_cache_key_includes_parameters(async); - - AssertSql( - """ -p0='London' -p1='Sales Representative' - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 -""", - // - """ -p0='Madrid' -p1='Accounting Manager' - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @p1 -"""); - } - - public override async Task SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(bool async) - { - await base.SqlQueryRaw_queryable_simple_as_no_tracking_not_composed(async); - - AssertSql( - """ -SELECT * FROM "Customers" -"""); - } - - public override async Task SqlQueryRaw_queryable_simple_projection_composed(bool async) - { - await base.SqlQueryRaw_queryable_simple_projection_composed(async); - - AssertSql( - """ -SELECT m."ProductName" -FROM ( - SELECT * - FROM "Products" - WHERE "Discontinued" <> TRUE - AND (("UnitsInStock" + "UnitsOnOrder") < "ReorderLevel") -) AS m -"""); - } - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] - public override async Task SqlQueryRaw_annotations_do_not_affect_successive_calls(bool async) - { - await base.SqlQueryRaw_annotations_do_not_affect_successive_calls(async); - - AssertSql( - """ -SELECT * FROM "Customers" WHERE "ContactName" LIKE '%z%' -""", - // - """ -SELECT * FROM "Customers" -"""); - } - - public override async Task SqlQueryRaw_with_dbParameter(bool async) - { - await base.SqlQueryRaw_with_dbParameter(async); - - AssertSql( - """ -@city='London' (Nullable = false) - -SELECT * FROM "Customers" WHERE "City" = @city -"""); - } - - public override async Task SqlQueryRaw_with_dbParameter_without_name_prefix(bool async) - { - await base.SqlQueryRaw_with_dbParameter_without_name_prefix(async); - AssertSql( - """ -city='London' (Nullable = false) - -SELECT * FROM "Customers" WHERE "City" = @city -"""); - } - - public override async Task SqlQueryRaw_with_dbParameter_mixed(bool async) - { - await base.SqlQueryRaw_with_dbParameter_mixed(async); - - AssertSql( - """ -p0='London' -@title='Sales Representative' (Nullable = false) - -SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @title -""", - // - """ -@city='London' (Nullable = false) -p0='Sales Representative' - -SELECT * FROM "Customers" WHERE "City" = @city AND "ContactTitle" = @p0 -"""); - } - - public override async Task SqlQueryRaw_with_db_parameters_called_multiple_times(bool async) - { - await base.SqlQueryRaw_with_db_parameters_called_multiple_times(async); - - AssertSql( - """ -@id='ALFKI' (Nullable = false) - -SELECT * FROM "Customers" WHERE "CustomerID" = @id -""", - // - """ -@id='ALFKI' (Nullable = false) - -SELECT * FROM "Customers" WHERE "CustomerID" = @id -"""); - } - - public override async Task SqlQuery_with_inlined_db_parameter(bool async) - { - await base.SqlQuery_with_inlined_db_parameter(async); - - AssertSql( - """ -@somename='ALFKI' (Nullable = false) - -SELECT * FROM "Customers" WHERE "CustomerID" = @somename -"""); - } - - public override async Task SqlQuery_with_inlined_db_parameter_without_name_prefix(bool async) - { - await base.SqlQuery_with_inlined_db_parameter_without_name_prefix(async); - - AssertSql( - """ -somename='ALFKI' (Nullable = false) - -SELECT * FROM "Customers" WHERE "CustomerID" = @somename -"""); - } - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] - public override async Task SqlQuery_parameterization_issue_12213(bool async) - { - await base.SqlQuery_parameterization_issue_12213(async); - - AssertSql( - """ -p0='10300' - -SELECT m."OrderID" -FROM ( - SELECT * FROM "Orders" WHERE "OrderID" >= @p0 -) AS m -""", - // - """ -@__max_1='10400' -p0='10300' - -SELECT m."OrderID" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."OrderID" <= @__max_1 AND EXISTS ( - SELECT 1 - FROM ( - SELECT * FROM "Orders" WHERE "OrderID" >= @p0 - ) AS m0 - WHERE m0."OrderID" = m."OrderID") -""", - // - """ -@__max_1='10400' -p0='10300' - -SELECT m."OrderID" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."OrderID" <= @__max_1 AND EXISTS ( - SELECT 1 - FROM ( - SELECT * FROM "Orders" WHERE "OrderID" >= @p0 - ) AS m0 - WHERE m0."OrderID" = m."OrderID") -"""); - } - - public override async Task SqlQueryRaw_does_not_parameterize_interpolated_string(bool async) - { - await base.SqlQueryRaw_does_not_parameterize_interpolated_string(async); - - AssertSql( - """ -p0='10250' - -SELECT * FROM "Orders" WHERE "OrderID" < @p0 -"""); - } - - public override async Task SqlQueryRaw_with_set_operation(bool async) - { - await base.SqlQueryRaw_with_set_operation(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT * FROM "Customers" WHERE "City" = 'London' -) AS m -UNION ALL -SELECT m0."Address", m0."City", m0."CompanyName", m0."ContactName", m0."ContactTitle", m0."Country", m0."CustomerID", m0."Fax", m0."Phone", m0."Region", m0."PostalCode" -FROM ( - SELECT * FROM "Customers" WHERE "City" = 'Berlin' -) AS m0 -"""); - } - - public override async Task Line_endings_after_Select(bool async) - { - await base.Line_endings_after_Select(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - SELECT - * FROM "Customers" -) AS m -WHERE m."City" = 'Seattle' -"""); - } - - public override async Task SqlQueryRaw_in_subquery_with_dbParameter(bool async) - { - await base.SqlQueryRaw_in_subquery_with_dbParameter(async); - - AssertSql( - """ -@city='London' (Nullable = false) - -SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."CustomerID" IN ( - SELECT m0."CustomerID" - FROM ( - SELECT * FROM "Customers" WHERE "City" = @city - ) AS m0 -) -"""); - } - - public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(bool async) - { - await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_without_name(async); - - AssertSql( - """ -p0='London' (Nullable = false) - -SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."CustomerID" IN ( - SELECT m0."CustomerID" - FROM ( - SELECT * FROM "Customers" WHERE "City" = @p0 - ) AS m0 -) -"""); - } - - public override async Task SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(bool async) - { - await base.SqlQueryRaw_in_subquery_with_positional_dbParameter_with_name(async); - - AssertSql( - """ -@city='London' (Nullable = false) - -SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."CustomerID" IN ( - SELECT m0."CustomerID" - FROM ( - SELECT * FROM "Customers" WHERE "City" = @city - ) AS m0 -) -"""); - } - - public override async Task SqlQueryRaw_with_dbParameter_mixed_in_subquery(bool async) - { - await base.SqlQueryRaw_with_dbParameter_mixed_in_subquery(async); - - AssertSql( - """ -p0='London' -@title='Sales Representative' (Nullable = false) - -SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."CustomerID" IN ( - SELECT m0."CustomerID" - FROM ( - SELECT * FROM "Customers" WHERE "City" = @p0 AND "ContactTitle" = @title - ) AS m0 -) -""", - // - """ -@city='London' (Nullable = false) -p0='Sales Representative' - -SELECT m."CustomerID", m."EmployeeID", m."Freight", m."OrderDate", m."OrderID", m."RequiredDate", m."ShipAddress", m."ShipCity", m."ShipCountry", m."ShipName", m."ShipPostalCode", m."ShipRegion", m."ShipVia", m."ShippedDate" -FROM ( - SELECT * FROM "Orders" -) AS m -WHERE m."CustomerID" IN ( - SELECT m0."CustomerID" - FROM ( - SELECT * FROM "Customers" WHERE "City" = @city AND "ContactTitle" = @p0 - ) AS m0 -) -"""); - } - - public override async Task SqlQueryRaw_composed_with_common_table_expression(bool async) - { - await base.SqlQueryRaw_composed_with_common_table_expression(async); - - AssertSql( - """ -SELECT m."Address", m."City", m."CompanyName", m."ContactName", m."ContactTitle", m."Country", m."CustomerID", m."Fax", m."Phone", m."Region", m."PostalCode" -FROM ( - WITH "Customers2" AS ( - SELECT * FROM "Customers" - ) - SELECT * FROM "Customers2" -) AS m -WHERE m."ContactName" LIKE '%z%' -"""); - } - -#pragma warning disable xUnit1026 - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] - public override Task Bad_data_error_handling_invalid_cast(bool async) - => Task.CompletedTask; - - [ConditionalTheory(Skip = "https://github.com/dotnet/efcore/pull/30384")] - public override Task Bad_data_error_handling_invalid_cast_projection(bool async) - => Task.CompletedTask; -#pragma warning restore xUnit1026 - - protected override DbParameter CreateDbParameter(string name, object value) - => new NpgsqlParameter { ParameterName = name, Value = value }; - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlFixture.cs deleted file mode 100644 index 8efe70c7d1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCFiltersInheritanceQueryNpgsqlFixture : TPCInheritanceQueryNpgsqlFixture -{ - public override bool EnableFilters - => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs deleted file mode 100644 index 26a0f386a0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCFiltersInheritanceQueryNpgsqlTest.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -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 deleted file mode 100644 index 075c16d7e8..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlFixture.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCGearsOfWarQueryNpgsqlFixture : TPCGearsOfWarQueryRelationalFixture -{ - static TPCGearsOfWarQueryNpgsqlFixture() - { - // TODO: Switch to using NpgsqlDataSource -#pragma warning disable CS0618 // Type or member is obsolete - NpgsqlConnection.GlobalTypeMapper.EnableRecordsAsTuples(); -#pragma warning restore CS0618 // Type or member is obsolete - } - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("uuid-ossp"); - - modelBuilder.Entity().Property(c => c.IssueDate).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); - } - - private GearsOfWarData? _expectedData; - - public override ISetSource GetExpectedData() - { - if (_expectedData is null) - { - _expectedData = (GearsOfWarData)base.GetExpectedData(); - - // 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. - foreach (var mission in _expectedData.Missions) - { - mission.Timeline = new DateTimeOffset( - mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - mission.Duration = new TimeSpan( - mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); - } - } - - return _expectedData; - } - - 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. - => SeedForNpgsqlAsync(context); - - public static async Task SeedForNpgsqlAsync(GearsOfWarContext context) - { - var squads = GearsOfWarData.CreateSquads(); - var missions = GearsOfWarData.CreateMissions(); - var squadMissions = GearsOfWarData.CreateSquadMissions(); - var cities = GearsOfWarData.CreateCities(); - var weapons = GearsOfWarData.CreateWeapons(); - var tags = GearsOfWarData.CreateTags(); - var gears = GearsOfWarData.CreateGears(); - var locustLeaders = GearsOfWarData.CreateLocustLeaders(); - var factions = GearsOfWarData.CreateFactions(); - var locustHighCommands = GearsOfWarData.CreateHighCommands(); - - foreach (var mission in missions) - { - mission.Timeline = new DateTimeOffset( - mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - mission.Duration = new TimeSpan( - mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); - } - - GearsOfWarData.WireUp( - squads, missions, squadMissions, cities, weapons, tags, gears, locustLeaders, factions, locustHighCommands); - - context.Squads.AddRange(squads); - context.Missions.AddRange(missions); - context.SquadMissions.AddRange(squadMissions); - context.Cities.AddRange(cities); - context.Weapons.AddRange(weapons); - context.Tags.AddRange(tags); - context.Gears.AddRange(gears); - context.LocustLeaders.AddRange(locustLeaders); - context.Factions.AddRange(factions); - context.LocustHighCommands.AddRange(locustHighCommands); - await context.SaveChangesAsync(); - - GearsOfWarData.WireUp2(locustLeaders, factions); - - await context.SaveChangesAsync(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs deleted file mode 100644 index 0ada4522f5..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCGearsOfWarQueryNpgsqlTest : TPCGearsOfWarQueryRelationalTestBase -{ - public TPCGearsOfWarQueryNpgsqlTest(TPCGearsOfWarQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // Base implementation uses DateTimeOffset.Now, which we don't translate by design. Use DateTimeOffset.UtcNow instead. - public override async Task Select_datetimeoffset_comparison_in_projection(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set().Select(m => m.Timeline > DateTimeOffset.UtcNow)); - - AssertSql( - """ -SELECT m."Timeline" > now() -FROM "Missions" AS m -"""); - } - - public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) - { - var dto = new DateTimeOffset(599898024001234567, new TimeSpan(0)); - var start = dto.AddDays(-1); - var end = dto.AddDays(1); - var dates = new[] { dto }; - - await AssertQuery( - async, - ss => ss.Set().Where( - m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline)), - assertEmpty: true); // TODO: Check this out - - AssertSql( - """ -@start='1902-01-01T10:00:00.1234567+00:00' (DbType = DateTime) -@end='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) -@dates={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) - -SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" -FROM "Missions" AS m -WHERE @start <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @end AND m."Timeline" = ANY (@dates) -"""); - } - - public override async Task DateTimeOffset_Date_returns_datetime(bool async) - { - var dateTimeOffset = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)); - - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Timeline.Date.ToLocalTime() >= dateTimeOffset.Date)); - - AssertSql( - """ -@dateTimeOffset_Date='0002-03-01T00:00:00.0000000' - -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 -"""); - } - - // Not supported by design: we support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a - // non-UTC DateTimeOffset. - public override Task DateTimeOffsetNow_minus_timespan(bool async) - => Assert.ThrowsAsync(() => base.DateTimeOffsetNow_minus_timespan(async)); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlFixture.cs deleted file mode 100644 index 32c62cd2d5..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCInheritanceQueryNpgsqlFixture : TPCInheritanceQueryFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs deleted file mode 100644 index c947da6058..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCInheritanceQueryNpgsqlTest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCInheritanceQueryNpgsqlTest(TPCInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : TPCInheritanceQueryTestBase(fixture, testOutputHelper) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyNoTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyNoTrackingQueryNpgsqlTest.cs deleted file mode 100644 index f9b4ae230a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyNoTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCManyToManyNoTrackingQueryNpgsqlTest : TPCManyToManyNoTrackingQueryRelationalTestBase -{ - public TPCManyToManyNoTrackingQueryNpgsqlTest(TPCManyToManyQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyQueryNpgsqlFixture.cs deleted file mode 100644 index cdccbb8a48..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyQueryNpgsqlFixture.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCManyToManyQueryNpgsqlFixture : TPCManyToManyQueryRelationalFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyQueryNpgsqlTest.cs deleted file mode 100644 index e275cee9f9..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCManyToManyQueryNpgsqlTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCManyToManyQueryNpgsqlTest : TPCManyToManyQueryRelationalTestBase -{ - public TPCManyToManyQueryNpgsqlTest(TPCManyToManyQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCRelationshipsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCRelationshipsQueryNpgsqlTest.cs deleted file mode 100644 index c39506cb76..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPCRelationshipsQueryNpgsqlTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPCRelationshipsQueryNpgsqlTest - : TPCRelationshipsQueryTestBase -{ - public TPCRelationshipsQueryNpgsqlTest( - TPCRelationshipsQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - fixture.TestSqlLoggerFactory.Clear(); - } - - public class TPCRelationshipsQueryNpgsqlFixture : TPCRelationshipsQueryRelationalFixture - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPHFiltersInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPHFiltersInheritanceQueryNpgsqlFixture.cs deleted file mode 100644 index 63e19c296d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPHFiltersInheritanceQueryNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPHFiltersInheritanceQueryNpgsqlFixture : TPHInheritanceQueryNpgsqlFixture -{ - public override bool EnableFilters - => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs deleted file mode 100644 index 8bae413c12..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlFixture.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPHInheritanceQueryNpgsqlFixture : TPHInheritanceQueryFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - 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 deleted file mode 100644 index 6e49fc9f3d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPHInheritanceQueryNpgsqlTest.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPHInheritanceQueryNpgsqlTest(TPHInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : TPHInheritanceQueryTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTFiltersInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPTFiltersInheritanceQueryNpgsqlFixture.cs deleted file mode 100644 index 1ce235de0d..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTFiltersInheritanceQueryNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTFiltersInheritanceQuerySqlServerFixture : TPTInheritanceQueryNpgsqlFixture -{ - public override bool EnableFilters - => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs deleted file mode 100644 index 40d55c7a7e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlFixture.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTGearsOfWarQueryNpgsqlFixture : TPTGearsOfWarQueryRelationalFixture -{ - static TPTGearsOfWarQueryNpgsqlFixture() - { - // TODO: Switch to using NpgsqlDataSource -#pragma warning disable CS0618 // Type or member is obsolete - NpgsqlConnection.GlobalTypeMapper.EnableRecordsAsTuples(); -#pragma warning restore CS0618 // Type or member is obsolete - } - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("uuid-ossp"); - - modelBuilder.Entity().Property(c => c.IssueDate).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(g => g.Location).HasColumnType("varchar(100)"); - } - - private GearsOfWarData? _expectedData; - - public override ISetSource GetExpectedData() - { - if (_expectedData is null) - { - _expectedData = (GearsOfWarData)base.GetExpectedData(); - - // 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. - foreach (var mission in _expectedData.Missions) - { - mission.Timeline = new DateTimeOffset( - mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - mission.Duration = new TimeSpan( - mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); - } - } - - return _expectedData; - } - - 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. - => SeedForNpgsqlAsync(context); - - public static async Task SeedForNpgsqlAsync(GearsOfWarContext context) - { - var squads = GearsOfWarData.CreateSquads(); - var missions = GearsOfWarData.CreateMissions(); - var squadMissions = GearsOfWarData.CreateSquadMissions(); - var cities = GearsOfWarData.CreateCities(); - var weapons = GearsOfWarData.CreateWeapons(); - var tags = GearsOfWarData.CreateTags(); - var gears = GearsOfWarData.CreateGears(); - var locustLeaders = GearsOfWarData.CreateLocustLeaders(); - var factions = GearsOfWarData.CreateFactions(); - var locustHighCommands = GearsOfWarData.CreateHighCommands(); - - foreach (var mission in missions) - { - mission.Timeline = new DateTimeOffset( - mission.Timeline.Ticks - (mission.Timeline.Ticks % (TimeSpan.TicksPerMillisecond / 1000)), TimeSpan.Zero); - mission.Duration = new TimeSpan( - mission.Duration.Ticks - (mission.Duration.Ticks % (TimeSpan.TicksPerMillisecond / 1000))); - } - - GearsOfWarData.WireUp( - squads, missions, squadMissions, cities, weapons, tags, gears, locustLeaders, factions, locustHighCommands); - - context.Squads.AddRange(squads); - context.Missions.AddRange(missions); - context.SquadMissions.AddRange(squadMissions); - context.Cities.AddRange(cities); - context.Weapons.AddRange(weapons); - context.Tags.AddRange(tags); - context.Gears.AddRange(gears); - context.LocustLeaders.AddRange(locustLeaders); - context.Factions.AddRange(factions); - context.LocustHighCommands.AddRange(locustHighCommands); - await context.SaveChangesAsync(); - - GearsOfWarData.WireUp2(locustLeaders, factions); - - await context.SaveChangesAsync(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs deleted file mode 100644 index ae01851071..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTGearsOfWarQueryNpgsqlTest : TPTGearsOfWarQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public TPTGearsOfWarQueryNpgsqlTest(TPTGearsOfWarQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // TODO: #1232 - // protected override bool CanExecuteQueryString => true; - - // Base implementation uses DateTimeOffset.Now, which we don't translate by design. Use DateTimeOffset.UtcNow instead. - public override async Task Select_datetimeoffset_comparison_in_projection(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set().Select(m => m.Timeline > DateTimeOffset.UtcNow)); - - AssertSql( - """ -SELECT m."Timeline" > now() -FROM "Missions" AS m -"""); - } - - public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool async) - { - var dto = new DateTimeOffset(599898024001234567, new TimeSpan(0)); - var start = dto.AddDays(-1); - var end = dto.AddDays(1); - var dates = new[] { dto }; - - await AssertQuery( - async, - ss => ss.Set().Where( - m => start <= m.Timeline.Date && m.Timeline < end && dates.Contains(m.Timeline)), - assertEmpty: true); // TODO: Check this out - - AssertSql( - """ -@start='1902-01-01T10:00:00.1234567+00:00' (DbType = DateTime) -@end='1902-01-03T10:00:00.1234567+00:00' (DbType = DateTime) -@dates={ '1902-01-02T10:00:00.1234567+00:00' } (DbType = Object) - -SELECT m."Id", m."CodeName", m."Date", m."Difficulty", m."Duration", m."Rating", m."Time", m."Timeline" -FROM "Missions" AS m -WHERE @start <= date_trunc('day', m."Timeline" AT TIME ZONE 'UTC')::timestamptz AND m."Timeline" < @end AND m."Timeline" = ANY (@dates) -"""); - } - - public override async Task DateTimeOffset_Date_returns_datetime(bool async) - { - var dateTimeOffset = new DateTimeOffset(2, 3, 1, 8, 0, 0, new TimeSpan(-5, 0, 0)); - - await AssertQuery( - async, - ss => ss.Set().Where(m => m.Timeline.Date.ToLocalTime() >= dateTimeOffset.Date)); - - AssertSql( - """ -@dateTimeOffset_Date='0002-03-01T00:00:00.0000000' - -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 -"""); - } - - // Not supported by design: we support getting a local DateTime via DateTime.Now (based on PG TimeZone), but there's no way to get a - // non-UTC DateTimeOffset. - public override Task DateTimeOffsetNow_minus_timespan(bool async) - => Assert.ThrowsAsync(() => base.DateTimeOffsetNow_minus_timespan(async)); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlFixture.cs deleted file mode 100644 index a3330b63f3..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTInheritanceQueryNpgsqlFixture : TPTInheritanceQueryFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs deleted file mode 100644 index 0639ec8967..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTInheritanceQueryNpgsqlTest.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTInheritanceQueryNpgsqlTest(TPTInheritanceQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : TPTInheritanceQueryTestBase(fixture, testOutputHelper); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyNoTrackingQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyNoTrackingQueryNpgsqlTest.cs deleted file mode 100644 index 89c59d3e1e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyNoTrackingQueryNpgsqlTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTManyToManyNoTrackingQueryNpgsqlTest : TPTManyToManyNoTrackingQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public TPTManyToManyNoTrackingQueryNpgsqlTest(TPTManyToManyQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // TODO: #1232 - // protected override bool CanExecuteQueryString => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyQueryNpgsqlFixture.cs deleted file mode 100644 index 686f9efc0a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyQueryNpgsqlFixture.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTManyToManyQueryNpgsqlFixture : TPTManyToManyQueryRelationalFixture -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.Key3).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.Payload).HasColumnType("timestamp without time zone"); - modelBuilder.Entity().Property(e => e.CompositeId3).HasColumnType("timestamp without time zone"); - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyQueryNpgsqlTest.cs deleted file mode 100644 index e11210b0b7..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTManyToManyQueryNpgsqlTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTManyToManyQueryNpgsqlTest : TPTManyToManyQueryRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public TPTManyToManyQueryNpgsqlTest(TPTManyToManyQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // TODO: #1232 - // protected override bool CanExecuteQueryString => true; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTRelationshipsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTRelationshipsQueryNpgsqlTest.cs deleted file mode 100644 index c656932398..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/TPTRelationshipsQueryNpgsqlTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class TPTRelationshipsQueryNpgsqlTest - : TPTRelationshipsQueryTestBase -{ - // ReSharper disable once UnusedParameter.Local - public TPTRelationshipsQueryNpgsqlTest( - TPTRelationshipsQueryNpgsqlFixture fixture, - ITestOutputHelper testOutputHelper) - : base(fixture) - { - fixture.TestSqlLoggerFactory.Clear(); - } - - public class TPTRelationshipsQueryNpgsqlFixture : TPTRelationshipsQueryRelationalFixture - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ToSqlQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/ToSqlQueryNpgsqlTest.cs deleted file mode 100644 index 96ea46d874..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/ToSqlQueryNpgsqlTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class ToSqlQuerySqlServerTest(NonSharedFixture fixture) : ToSqlQueryTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - // Base test implementation does not properly use identifier delimiters in raw SQL and isn't usable on PostgreSQL - public override Task Entity_type_with_navigation_mapped_to_SqlQuery(bool async) - => Task.CompletedTask; - - private void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/BasicTypesQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/BasicTypesQueryNpgsqlFixture.cs deleted file mode 100644 index e3c5dcccfd..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/BasicTypesQueryNpgsqlFixture.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class BasicTypesQueryNpgsqlFixture : BasicTypesQueryFixtureBase, ITestSqlLoggerFactory -{ - private BasicTypesData? _expectedData; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder(base.AddOptions(builder)).SetPostgresVersion(TestEnvironment.PostgresVersion); - return builder; - } - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - => modelBuilder.HasPostgresExtension("uuid-ossp"); - - protected override Task SeedAsync(BasicTypesContext context) - { - _expectedData ??= LoadAndTweakData(); - context.AddRange(_expectedData.BasicTypesEntities); - context.AddRange(_expectedData.NullableBasicTypesEntities); - return context.SaveChangesAsync(); - } - - public override ISetSource GetExpectedData() - => _expectedData ??= LoadAndTweakData(); - - private BasicTypesData LoadAndTweakData() - { - var data = (BasicTypesData)base.GetExpectedData(); - - foreach (var item in data.BasicTypesEntities) - { - // For all relevant temporal types, chop sub-microsecond precision which PostgreSQL does not support. - // Temporal types which aren't set (default) get mapped to -infinity on PostgreSQL; this value causes many tests to fail. - - if (item.DateTime == default) - { - item.DateTime += TimeSpan.FromSeconds(1); - } - - // PostgreSQL maps DateTime to timestamptz by default, but that represents UTC timestamps which require DateTimeKind.Utc. - item.DateTime = DateTime.SpecifyKind(new DateTime(StripSubMicrosecond(item.DateTime.Ticks)), DateTimeKind.Utc); - - if (item.DateOnly == default) - { - item.DateOnly = item.DateOnly.AddDays(1); - } - - item.TimeOnly = new TimeOnly(StripSubMicrosecond(item.TimeOnly.Ticks)); - item.TimeSpan = new TimeSpan(StripSubMicrosecond(item.TimeSpan.Ticks)); - - if (item.DateTimeOffset == default) - { - item.DateTimeOffset += TimeSpan.FromSeconds(1); - } - - // PostgreSQL doesn't have a real DateTimeOffset type; we map .NET DateTimeOffset to timestamptz, which represents a UTC - // timestamp, and so we only support offset=0. - // Also chop sub-microsecond precision which PostgreSQL does not support. - item.DateTimeOffset = new DateTimeOffset(StripSubMicrosecond(item.DateTimeOffset.Ticks), TimeSpan.Zero); - } - - // Do the same for the nullable counterparts - foreach (var item in data.NullableBasicTypesEntities) - { - if (item.DateTime.HasValue) - { - item.DateTime = DateTime.SpecifyKind(new DateTime(StripSubMicrosecond(item.DateTime.Value.Ticks)), DateTimeKind.Utc); - } - - if (item.TimeOnly.HasValue) - { - item.TimeOnly = new TimeOnly(StripSubMicrosecond(item.TimeOnly.Value.Ticks)); - } - - if (item.TimeSpan.HasValue) - { - item.TimeSpan = new TimeSpan(StripSubMicrosecond(item.TimeSpan.Value.Ticks)); - } - - if (item.DateTimeOffset.HasValue) - { - item.DateTimeOffset = new DateTimeOffset(StripSubMicrosecond(item.DateTimeOffset.Value.Ticks), TimeSpan.Zero); - } - } - - return data; - - static long StripSubMicrosecond(long ticks) => ticks - (ticks % (TimeSpan.TicksPerMillisecond / 1000)); - } - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/ByteArrayTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/ByteArrayTranslationsNpgsqlTest.cs deleted file mode 100644 index c280c208d6..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/ByteArrayTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class ByteArrayTranslationsNpgsqlTest : ByteArrayTranslationsTestBase -{ - // ReSharper disable once UnusedParameter.Local - public ByteArrayTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Length() - { - await base.Length(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."ByteArray") = 4 -"""); - } - - public override async Task Index() - { - await base.Index(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."ByteArray") >= 3 AND get_byte(b."ByteArray", 2) = 190 -"""); - } - - public override async Task First() - { - await base.First(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."ByteArray") >= 1 AND get_byte(b."ByteArray", 0)::smallint = 222 -"""); - } - - public override async Task Contains_with_constant() - { - await base.Contains_with_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE position(BYTEA E'\\x01' IN b."ByteArray") > 0 -"""); - } - - public override async Task Contains_with_parameter() - { - await base.Contains_with_parameter(); - - AssertSql( - """ -@someByte='1' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE position(set_byte(BYTEA E'\\x00', 0, @someByte) IN b."ByteArray") > 0 -"""); - } - - public override async Task Contains_with_column() - { - await base.Contains_with_column(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE position(set_byte(BYTEA E'\\x00', 0, b."Byte") IN b."ByteArray") > 0 -"""); - } - - public override async Task SequenceEqual() - { - await base.SequenceEqual(); - - AssertSql( - """ -@byteArrayParam='0xDEADBEEF' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."ByteArray" = @byteArrayParam -"""); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/CubeTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/CubeTranslationsTest.cs deleted file mode 100644 index ccadc2f72b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/CubeTranslationsTest.cs +++ /dev/null @@ -1,1388 +0,0 @@ -using NpgsqlTypes; - -// ReSharper disable InconsistentNaming - -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -/// -/// Tests operations on the PostgreSQL cube type. -/// -[MinimumPostgresVersion(14, 0)] // Binary conversion for cube type was added in PostgreSQL 14 -public class CubeTranslationsTest : IClassFixture -{ - private CubeQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public CubeTranslationsTest(CubeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Operators - - [ConditionalFact] - public void Contains_cube() - { - using var context = CreateContext(); - var smallCube = new NpgsqlCube([2.0, 3.0, 4.0], [3.0, 4.0, 5.0]); - var result = context.CubeTestEntities.Single(x => x.Cube.Contains(smallCube)); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@smallCube='(2, 3, 4),(3, 4, 5)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" @> @smallCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Contains_point() - { - using var context = CreateContext(); - var point = new NpgsqlCube([2.0, 3.0, 4.0]); - var result = context.CubeTestEntities.Single(x => x.Cube.Contains(point)); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@point='(2, 3, 4)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" @> @point -LIMIT 2 -"""); - } - - [ConditionalFact] - public void ContainedBy_cube() - { - using var context = CreateContext(); - // Cube that contains Id=1 (1,2,3),(4,5,6) - // Filter by dimension to exclude 1D cube (Id=5) - var largeCube = new NpgsqlCube([0.5, 1.5, 2.5], [4.5, 5.5, 6.5]); - var result = context.CubeTestEntities - .Single(x => x.Cube.ContainedBy(largeCube) && x.Cube.Dimensions == 3); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@largeCube='(0.5, 1.5, 2.5),(4.5, 5.5, 6.5)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" <@ @largeCube AND cube_dim(c."Cube") = 3 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void ContainedBy_point() - { - using var context = CreateContext(); - var cube = new NpgsqlCube([6.0, 7.0, 8.0], [8.0, 9.0, 10.0]); - var result = context.CubeTestEntities.Single(x => x.Cube.ContainedBy(cube)); - Assert.Equal(2, result.Id); - - AssertSql( - """ -@cube='(6, 7, 8),(8, 9, 10)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" <@ @cube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Overlaps() - { - using var context = CreateContext(); - var overlappingCube = new NpgsqlCube([0.0, 1.0, 2.0], [5.0, 6.0, 7.0]); - var result = context.CubeTestEntities.Single(x => x.Cube.Overlaps(overlappingCube)); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@overlappingCube='(0, 1, 2),(5, 6, 7)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" && @overlappingCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Distance() - { - using var context = CreateContext(); - var targetCube = new NpgsqlCube([5.0, 6.0, 7.0]); - var result = context.CubeTestEntities.OrderBy(x => x.Cube.Distance(targetCube)).First(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@targetCube='(5, 6, 7)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -ORDER BY c."Cube" <-> @targetCube NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void DistanceTaxicab() - { - using var context = CreateContext(); - var targetCube = new NpgsqlCube([5.0, 6.0, 7.0]); - var result = context.CubeTestEntities.OrderBy(x => x.Cube.DistanceTaxicab(targetCube)).First(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@targetCube='(5, 6, 7)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -ORDER BY c."Cube" <#> @targetCube NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void DistanceChebyshev() - { - using var context = CreateContext(); - var targetCube = new NpgsqlCube([5.0, 6.0, 7.0]); - var result = context.CubeTestEntities.OrderBy(x => x.Cube.DistanceChebyshev(targetCube)).First(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@targetCube='(5, 6, 7)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -ORDER BY c."Cube" <=> @targetCube NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void NthCoordinate() - { - using var context = CreateContext(); - // Zero-based index 0 should translate to PostgreSQL index 1 - var result = context.CubeTestEntities.Where(x => x.Cube.NthCoordinate(0) == 1.0).Single(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" -> 1 = 1.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void NthCoordinateKnn() - { - using var context = CreateContext(); - // Zero-based index 1 should translate to PostgreSQL index 2 - // The ~> operator uses flattened representation: odd indices=lower-left, even=upper-right - // Index 2 accesses upper-right coordinate of first dimension = 4.0 for cube (1,2,3),(4,5,6) - var result = context.CubeTestEntities.Where(x => x.Cube.NthCoordinateKnn(1) == 4.0).Single(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" ~> 2 = 4.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void LowerLeft_indexer_different_dimension() - { - using var context = CreateContext(); - // Zero-based index 1 should translate to PostgreSQL index 2 - // LowerLeft[1] accesses the second lower-left coordinate = 2.0 for cube (1,2,3),(4,5,6) - var result = context.CubeTestEntities.Where(x => x.Cube.LowerLeft[1] == 2.0).Single(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ll_coord(c."Cube", 2) = 2.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void UpperRight_indexer_different_dimension() - { - using var context = CreateContext(); - // Zero-based index 1 should translate to PostgreSQL index 2 - // UpperRight[1] accesses the second upper-right coordinate = 5.0 for cube (1,2,3),(4,5,6) - var result = context.CubeTestEntities.Where(x => x.Cube.UpperRight[1] == 5.0).Single(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ur_coord(c."Cube", 2) = 5.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void LowerLeft_and_UpperRight_indexers_combined() - { - using var context = CreateContext(); - // Combine both LowerLeft and UpperRight indexers in the same query - // For cube (1,2,3),(4,5,6): LowerLeft[0]=1.0, UpperRight[0]=4.0 - var result = context.CubeTestEntities.Where(x => x.Cube.LowerLeft[0] == 1.0 && x.Cube.UpperRight[0] == 4.0).Single(); - Assert.Equal(1, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ll_coord(c."Cube", 1) = 1.0 AND cube_ur_coord(c."Cube", 1) = 4.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Equals_operator() - { - using var context = CreateContext(); - var cube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]); - var result = context.CubeTestEntities.Single(x => x.Cube == cube); - Assert.Equal(1, result.Id); - - AssertSql( - """ -@cube='(1, 2, 3),(4, 5, 6)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" = @cube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void NotEquals_operator() - { - using var context = CreateContext(); - var cube = new NpgsqlCube(new[] { 1.0, 2.0, 3.0 }, [4.0, 5.0, 6.0]); - var results = context.CubeTestEntities.Where(x => x.Cube != cube).ToList(); - Assert.Equal(5, results.Count); // All except Id=1 - - AssertSql( - """ -@cube='(1, 2, 3),(4, 5, 6)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" <> @cube -"""); - } - - #endregion - - #region Properties - - [ConditionalFact] - public void Dimensions_property() - { - using var context = CreateContext(); - var results = context.CubeTestEntities.Where(x => x.Cube.Dimensions == 3).ToList(); - Assert.Equal(5, results.Count); // Id 1, 2, 3, 4, 6 all have 3 dimensions - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 -"""); - } - - [ConditionalFact] - public void IsPoint_property() - { - using var context = CreateContext(); - var results = context.CubeTestEntities.Where(x => x.Cube.IsPoint).ToList(); - Assert.Equal(2, results.Count); - Assert.Contains(results, r => r.Id == 2); - Assert.Contains(results, r => r.Id == 5); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_is_point(c."Cube") -"""); - } - - #endregion - - #region Functions - - [ConditionalFact] - public void Union() - { - using var context = CreateContext(); - var cube2 = new NpgsqlCube([7.0, 8.0, 9.0]); - // Call Union on database column (x.Cube) with a parameter to avoid client evaluation - var result = context.CubeTestEntities - .Where(x => x.Id == 1) - .Select(x => x.Cube.Union(cube2)) - .First(); - - AssertSql( - """ -@cube2='(7, 8, 9)' (DbType = Object) - -SELECT cube_union(c."Cube", @cube2) -FROM "CubeTestEntities" AS c -WHERE c."Id" = 1 -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Intersect() - { - using var context = CreateContext(); - var cube2 = new NpgsqlCube([2.0, 3.0, 4.0], [8.0, 9.0, 10.0]); - // Call Intersect on database column (x.Cube) with a parameter to avoid client evaluation - var result = context.CubeTestEntities - .Where(x => x.Id == 1) - .Select(x => x.Cube.Intersect(cube2)) - .First(); - - AssertSql( - """ -@cube2='(2, 3, 4),(8, 9, 10)' (DbType = Object) - -SELECT cube_inter(c."Cube", @cube2) -FROM "CubeTestEntities" AS c -WHERE c."Id" = 1 -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Enlarge() - { - using var context = CreateContext(); - // Call Enlarge on database column (x.Cube) to avoid client evaluation - var result = context.CubeTestEntities - .Where(x => x.Id == 1) - .Select(x => x.Cube.Enlarge(1.0, 3)) - .First(); - - AssertSql( - """ -SELECT cube_enlarge(c."Cube", 1.0, 3) -FROM "CubeTestEntities" AS c -WHERE c."Id" = 1 -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Enlarge_negative() - { - using var context = CreateContext(); - // Call Enlarge on database column (x.Cube) with negative radius to shrink - var result = context.CubeTestEntities - .Where(x => x.Id == 1) - .Select(x => x.Cube.Enlarge(-1.0, 3)) - .First(); - - AssertSql( - """ -SELECT cube_enlarge(c."Cube", -1.0, 3) -FROM "CubeTestEntities" AS c -WHERE c."Id" = 1 -LIMIT 1 -"""); - } - - [ConditionalFact] - public void ToSubset_extract() - { - using var context = CreateContext(); - // Extract dimension 0 (PostgreSQL index 1) - expected result is (1),(4) - var subset = new NpgsqlCube([1.0], [4.0]); - var result = context.CubeTestEntities.Where(x => x.Cube.ToSubset(0) == subset).ToList(); - - AssertSql( - """ -@subset='(1),(4)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_subset(c."Cube", ARRAY[1]::integer[]) = @subset -"""); - } - - [ConditionalFact] - public void ToSubset_reorder() - { - using var context = CreateContext(); - // Reorder dimensions: [2, 1, 0] (PostgreSQL: [3, 2, 1]) - expected result is (3,2,1),(6,5,4) - var reordered = new NpgsqlCube([3.0, 2.0, 1.0], [6.0, 5.0, 4.0]); - // Filter by dimension to avoid errors on cubes with different dimensionality - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(2, 1, 0) == reordered) - .ToList(); - - AssertSql( - """ -@reordered='(3, 2, 1),(6, 5, 4)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_subset(c."Cube", ARRAY[3,2,1]::integer[]) = @reordered -"""); - } - - [ConditionalFact] - public void ToSubset_duplicate() - { - using var context = CreateContext(); - // Duplicate dimension 0: [0, 0, 1] (PostgreSQL: [1, 1, 2]) - expected result is (1,1,2),(4,4,5) - var duplicated = new NpgsqlCube([1.0, 1.0, 2.0], [4.0, 4.0, 5.0]); - // Filter by dimension to avoid errors on cubes with different dimensionality - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(0, 0, 1) == duplicated) - .ToList(); - - AssertSql( - """ -@duplicated='(1, 1, 2),(4, 4, 5)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_subset(c."Cube", ARRAY[1,1,2]::integer[]) = @duplicated -"""); - } - - [ConditionalFact] - public void ToSubset_with_parameter_array_single_index() - { - using var context = CreateContext(); - // Test parameter array conversion: zero-based [0] should become one-based [1] in SQL - var indexes = new[] { 0 }; - var subset = new NpgsqlCube([1.0], [4.0]); - var result = context.CubeTestEntities.Where(x => x.Cube.ToSubset(indexes) == subset).ToList(); - - AssertSql( - """ -@indexes={ '0' } (DbType = Object) -@subset='(1),(4)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_subset(c."Cube", ( - SELECT array_agg(u.x + 1) - FROM unnest(@indexes) AS u(x))) = @subset -"""); - } - - [ConditionalFact] - public void ToSubset_with_parameter_array_multiple_indexes() - { - using var context = CreateContext(); - // Test parameter array conversion with reordering: [2, 1, 0] should become [3, 2, 1] in SQL - var indexes = new[] { 2, 1, 0 }; - var reordered = new NpgsqlCube([3.0, 2.0, 1.0], [6.0, 5.0, 4.0]); - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(indexes) == reordered) - .ToList(); - - AssertSql( - """ -@indexes={ '2' -'1' -'0' } (DbType = Object) -@reordered='(3, 2, 1),(6, 5, 4)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_subset(c."Cube", ( - SELECT array_agg(u.x + 1) - FROM unnest(@indexes) AS u(x))) = @reordered -"""); - } - - [ConditionalFact] - public void ToSubset_with_inline_array_literal() - { - using var context = CreateContext(); - // Test inline array literal: new[] { 0, 1 } should be converted at translation time - var extracted = new NpgsqlCube([1.0, 2.0], [4.0, 5.0]); - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(new[] { 0, 1 }) == extracted) - .ToList(); - - AssertSql( - """ -@extracted='(1, 2),(4, 5)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_subset(c."Cube", ARRAY[1,2]::integer[]) = @extracted -"""); - } - - [ConditionalFact] - public void ToSubset_with_empty_array_parameter() - { - using var context = CreateContext(); - // Test empty array parameter - PostgreSQL should handle this gracefully - var indexes = Array.Empty(); - var result = context.CubeTestEntities - .Where(x => x.Cube.ToSubset(indexes).Dimensions == 0) - .ToList(); - - AssertSql( - """ -@indexes={ } (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(cube_subset(c."Cube", ( - SELECT array_agg(u.x + 1) - FROM unnest(@indexes) AS u(x)))) = 0 -"""); - } - - [ConditionalFact] - public void ToSubset_with_repeated_indexes_in_parameter() - { - using var context = CreateContext(); - // Test parameter array with repeated indices - duplicates should be converted correctly - var indexes = new[] { 0, 1, 0, 2, 1 }; - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(indexes).Dimensions == 5) - .ToList(); - - AssertSql( - """ -@indexes={ '0' -'1' -'0' -'2' -'1' } (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_dim(cube_subset(c."Cube", ( - SELECT array_agg(u.x + 1) - FROM unnest(@indexes) AS u(x)))) = 5 -"""); - } - - [ConditionalFact] - public void ToSubset_with_column_array() - { - using var context = CreateContext(); - // Test column-based array: should generate runtime subquery conversion - var result = context.CubeTestEntities - .Where(x => x.Id == 1 && x.Cube.ToSubset(x.IndexArray!).Dimensions == 2) - .ToList(); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Id" = 1 AND cube_dim(cube_subset(c."Cube", ( - SELECT array_agg(u.x + 1) - FROM unnest(c."IndexArray") AS u(x)))) = 2 -"""); - } - - [ConditionalFact] - public void ToSubset_with_column_array_multiple_rows() - { - using var context = CreateContext(); - // Test column-based array with multiple rows - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(x.IndexArray!).Dimensions == 2) - .ToList(); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_dim(cube_subset(c."Cube", ( - SELECT array_agg(u.x + 1) - FROM unnest(c."IndexArray") AS u(x)))) = 2 -"""); - } - - [ConditionalFact] - public void ToSubset_with_mixed_constant_and_variable_indexes() - { - using var context = CreateContext(); - var variableIndex = 1; - var extracted = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]); - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.ToSubset(new[] { 0, variableIndex, 2 }) == extracted) - .ToList(); - - AssertSql( - """ -@variableIndex='1' -@extracted='(1, 2, 3),(4, 5, 6)' (DbType = Object) - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_subset(c."Cube", ARRAY[1,@variableIndex + 1,3]::integer[]) = @extracted -"""); - } - - #endregion - - #region Edge Cases - - [ConditionalFact] - public void Single_dimension_cube() - { - using var context = CreateContext(); - var result = context.CubeTestEntities.Where(x => x.Cube.Dimensions == 1).Single(); - Assert.Equal(5, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 1 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Negative_coordinates() - { - using var context = CreateContext(); - // Find cubes with any negative coordinate - var result = context.CubeTestEntities.Where(x => x.Cube.NthCoordinate(0) < 0).Single(); - Assert.Equal(6, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" -> 1 < 0.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Zero_volume_cube_point() - { - using var context = CreateContext(); - var results = context.CubeTestEntities.Where(x => x.Cube.IsPoint).ToList(); - Assert.Equal(2, results.Count); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_is_point(c."Cube") -"""); - } - - [ConditionalFact] - public void Large_coordinates() - { - using var context = CreateContext(); - // Find the cube with large coordinates (Id = 4) - var result = context.CubeTestEntities.Where(x => x.Cube.NthCoordinate(0) > 50.0).Single(); - Assert.Equal(4, result.Id); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE c."Cube" -> 1 > 50.0 -LIMIT 2 -"""); - } - - [ConditionalFact] - public void LowerLeft_Count_throws_helpful_error() - { - using var context = CreateContext(); - var exception = Assert.Throws( - () => context.CubeTestEntities.Where(x => x.Cube.LowerLeft.Count > 0).ToList()); - - Assert.Contains("LowerLeft", exception.Message); - Assert.Contains("cannot be translated to SQL", exception.Message); - Assert.Contains("indexer syntax", exception.Message); - } - - [ConditionalFact] - public void LowerLeft_index_with_constant() - { - using var context = CreateContext(); - var result = context.CubeTestEntities - .Where(x => x.Cube.LowerLeft[0] == 1.0) - .ToList(); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ll_coord(c."Cube", 1) = 1.0 -"""); - } - - [ConditionalFact] - public void UpperRight_Count_throws_helpful_error() - { - using var context = CreateContext(); - var exception = Assert.Throws( - () => context.CubeTestEntities.Where(x => x.Cube.UpperRight.Count > 0).ToList()); - - Assert.Contains("UpperRight", exception.Message); - Assert.Contains("cannot be translated to SQL", exception.Message); - Assert.Contains("indexer syntax", exception.Message); - } - - [ConditionalFact] - public void LowerLeft_in_projection_throws_helpful_error() - { - using var context = CreateContext(); - var exception = Assert.Throws( - () => context.CubeTestEntities - .Select(x => new { x.Id, LowerLeft = x.Cube.LowerLeft }) - .ToList()); - - Assert.Contains("LowerLeft", exception.Message); - Assert.Contains("cannot be translated to SQL", exception.Message); - Assert.Contains("indexer syntax", exception.Message); - } - - [ConditionalFact] - public void UpperRight_in_projection_throws_helpful_error() - { - using var context = CreateContext(); - var exception = Assert.Throws( - () => context.CubeTestEntities - .Select(x => new { x.Id, UpperRight = x.Cube.UpperRight }) - .ToList()); - - Assert.Contains("UpperRight", exception.Message); - Assert.Contains("cannot be translated to SQL", exception.Message); - Assert.Contains("indexer syntax", exception.Message); - } - - [ConditionalFact] - public void LowerLeft_FirstOrDefault_throws_helpful_error() - { - using var context = CreateContext(); - var exception = Assert.Throws( - () => context.CubeTestEntities - .Select(x => x.Cube.LowerLeft.FirstOrDefault()) - .ToList()); - - Assert.Contains("LowerLeft", exception.Message); - Assert.Contains("cannot be translated to SQL", exception.Message); - Assert.Contains("indexer syntax", exception.Message); - } - - [ConditionalFact] - public void UpperRight_Any_throws_helpful_error() - { - using var context = CreateContext(); - var exception = Assert.Throws( - () => context.CubeTestEntities - .Where(x => x.Cube.UpperRight.Any(coord => coord > 5.0)) - .ToList()); - - Assert.Contains("UpperRight", exception.Message); - Assert.Contains("cannot be translated to SQL", exception.Message); - Assert.Contains("indexer syntax", exception.Message); - } - - [ConditionalFact] - public void UpperRight_index_with_constant() - { - using var context = CreateContext(); - var result = context.CubeTestEntities - .Where(x => x.Cube.UpperRight[2] > 5.0) - .ToList(); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ur_coord(c."Cube", 3) > 5.0 -"""); - } - - [ConditionalFact] - public void LowerLeft_index_with_parameter() - { - using var context = CreateContext(); - var index = 1; - var result = context.CubeTestEntities - .Where(x => x.Cube.LowerLeft[index] < 3.0) - .ToList(); - - AssertSql( - """ -@index='1' - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ll_coord(c."Cube", @index + 1) < 3.0 -"""); - } - - [ConditionalFact] - public void UpperRight_index_with_parameter() - { - using var context = CreateContext(); - var index = 2; - var result = context.CubeTestEntities - .Where(x => x.Cube.UpperRight[index] > 5.0) - .ToList(); - - AssertSql( - """ -@index='2' - -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_ur_coord(c."Cube", @index + 1) > 5.0 -"""); - } - - [ConditionalFact] - public void LowerLeft_index_with_column() - { - using var context = CreateContext(); - // Use Id % 3 to get a valid index (0, 1, or 2) for the 3D cubes in test data - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.LowerLeft[x.Id % 3] > 0.0) - .ToList(); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_ll_coord(c."Cube", c."Id" % 3 + 1) > 0.0 -"""); - } - - [ConditionalFact] - public void UpperRight_index_with_column() - { - using var context = CreateContext(); - // Use Id % 3 to get a valid index (0, 1, or 2) for the 3D cubes in test data - var result = context.CubeTestEntities - .Where(x => x.Cube.Dimensions == 3 && x.Cube.UpperRight[x.Id % 3] < 10.0) - .ToList(); - - AssertSql( - """ -SELECT c."Id", c."Cube", c."IndexArray" -FROM "CubeTestEntities" AS c -WHERE cube_dim(c."Cube") = 3 AND cube_ur_coord(c."Cube", c."Id" % 3 + 1) < 10.0 -"""); - } - - #endregion - - #region Constructors - - [ConditionalFact] - public void Constructor_in_where_point() - { - using var context = CreateContext(); - // Use constructor in WHERE clause - this exercises the translation when comparing - var targetCube = new NpgsqlCube(10.0); - var result = context.CubeConstructorTestEntities - .Where(x => new NpgsqlCube(x.Coord1) == targetCube) - .Select(x => x.Id) - .Single(); - - Assert.Equal(1, result); - - AssertSql( - """ -@targetCube='(10)' (DbType = Object) - -SELECT c."Id" -FROM "CubeConstructorTestEntities" AS c -WHERE cube(c."Coord1") = @targetCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Constructor_in_where_one_dimension() - { - using var context = CreateContext(); - var targetCube = new NpgsqlCube(10.0, 20.0); - var result = context.CubeConstructorTestEntities - .Where(x => new NpgsqlCube(x.Coord1, x.Coord2) == targetCube) - .Select(x => x.Id) - .Single(); - - Assert.Equal(1, result); - - AssertSql( - """ -@targetCube='(10),(20)' (DbType = Object) - -SELECT c."Id" -FROM "CubeConstructorTestEntities" AS c -WHERE cube(c."Coord1", c."Coord2") = @targetCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Constructor_in_where_zero_volume() - { - using var context = CreateContext(); - var targetCube = new NpgsqlCube([10.0, 20.0, 30.0]); - var result = context.CubeConstructorTestEntities - .Where(x => new NpgsqlCube(x.Coordinates) == targetCube) - .Select(x => x.Id) - .Single(); - - Assert.Equal(1, result); - - AssertSql( - """ -@targetCube='(10, 20, 30)' (DbType = Object) - -SELECT c."Id" -FROM "CubeConstructorTestEntities" AS c -WHERE cube(c."Coordinates") = @targetCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Constructor_in_where_lower_left_upper_right() - { - using var context = CreateContext(); - var targetCube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]); - var result = context.CubeConstructorTestEntities - .Where(x => new NpgsqlCube(x.LowerLeft, x.UpperRight) == targetCube) - .Select(x => x.Id) - .Single(); - - Assert.Equal(1, result); - - AssertSql( - """ -@targetCube='(1, 2, 3),(4, 5, 6)' (DbType = Object) - -SELECT c."Id" -FROM "CubeConstructorTestEntities" AS c -WHERE cube(c."LowerLeft", c."UpperRight") = @targetCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Constructor_extend_cube_one_coordinate() - { - using var context = CreateContext(); - var baseCube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]); - var targetCube = new NpgsqlCube(baseCube, 40.0); - var result = context.CubeConstructorTestEntities - .Where(x => new NpgsqlCube(x.BaseCube, x.Coord1) == targetCube) - .Select(x => x.Id) - .Single(); - - Assert.Equal(2, result); - - AssertSql( - """ -@targetCube='(1, 2, 3, 40),(4, 5, 6, 40)' (DbType = Object) - -SELECT c."Id" -FROM "CubeConstructorTestEntities" AS c -WHERE cube(c."BaseCube", c."Coord1") = @targetCube -LIMIT 2 -"""); - } - - [ConditionalFact] - public void Constructor_extend_cube_two_coordinates() - { - using var context = CreateContext(); - var baseCube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]); - var targetCube = new NpgsqlCube(baseCube, 40.0, 50.0); - var result = context.CubeConstructorTestEntities - .Where(x => new NpgsqlCube(x.BaseCube, x.Coord1, x.Coord2) == targetCube) - .Select(x => x.Id) - .Single(); - - Assert.Equal(2, result); - - AssertSql( - """ -@targetCube='(1, 2, 3, 40),(4, 5, 6, 50)' (DbType = Object) - -SELECT c."Id" -FROM "CubeConstructorTestEntities" AS c -WHERE cube(c."BaseCube", c."Coord1", c."Coord2") = @targetCube -LIMIT 2 -"""); - } - - #endregion - - #region Constructor NULL handling - - [ConditionalFact] - public void Constructor_single_coordinate_null() - { - using var context = CreateContext(); - double? nullCoord = null; - - // cube(NULL) should return NULL (STRICT function) - var result = context.CubeConstructorTestEntities - .Select(x => new { x.Id, Cube = nullCoord.HasValue ? new NpgsqlCube(nullCoord.Value) : (NpgsqlCube?)null }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Constructor_two_coordinates_first_null() - { - using var context = CreateContext(); - double? nullCoord = null; - double secondCoord = 20.0; - - // cube(NULL, 20.0) should return NULL (STRICT function) - var result = context.CubeConstructorTestEntities - .Select(x => new { - x.Id, - Cube = nullCoord.HasValue ? new NpgsqlCube(nullCoord.Value, secondCoord) : (NpgsqlCube?)null - }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Constructor_two_coordinates_second_null() - { - using var context = CreateContext(); - double firstCoord = 10.0; - double? nullCoord = null; - - // cube(10.0, NULL) should return NULL (STRICT function) - var result = context.CubeConstructorTestEntities - .Select(x => new { - x.Id, - Cube = nullCoord.HasValue ? new NpgsqlCube(firstCoord, nullCoord.Value) : (NpgsqlCube?)null - }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Constructor_array_null() - { - using var context = CreateContext(); - double[]? nullArray = null; - - var result = context.CubeConstructorTestEntities - .Select(x => new { - x.Id, - Cube = nullArray != null ? new NpgsqlCube(nullArray) : (NpgsqlCube?)null - }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Constructor_two_arrays_first_null() - { - using var context = CreateContext(); - double[]? nullArray = null; - double[] secondArray = [4.0, 5.0, 6.0]; - - var result = context.CubeConstructorTestEntities - .Select(x => new { - x.Id, - Cube = nullArray != null ? new NpgsqlCube(nullArray, secondArray) : (NpgsqlCube?)null - }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Constructor_extend_cube_null_cube() - { - using var context = CreateContext(); - NpgsqlCube? nullCube = null; - const double coord = 7.0; - - var result = context.CubeConstructorTestEntities - .Select(x => new { - x.Id, - Cube = nullCube.HasValue ? new NpgsqlCube(nullCube.Value, coord) : (NpgsqlCube?)null - }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - [ConditionalFact] - public void Constructor_extend_cube_null_coord() - { - using var context = CreateContext(); - var cube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]); - double? nullCoord = null; - - var result = context.CubeConstructorTestEntities - .Select(x => new { - x.Id, - Cube = nullCoord.HasValue ? new NpgsqlCube(cube, nullCoord.Value) : (NpgsqlCube?)null - }) - .OrderBy(x => x.Id) - .First(); - - Assert.Null(result.Cube); - - AssertSql( - """ -SELECT c."Id", NULL AS "Cube" -FROM "CubeConstructorTestEntities" AS c -ORDER BY c."Id" NULLS FIRST -LIMIT 1 -"""); - } - - #endregion - - #region Fixture - - public class CubeQueryNpgsqlFixture : SharedStoreFixtureBase - { - protected override string StoreName - => "CubeQueryTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override Task SeedAsync(CubeContext context) - => CubeContext.SeedAsync(context); - } - - public class CubeTestEntity - { - public int Id { get; set; } - public NpgsqlCube Cube { get; set; } - public int[]? IndexArray { get; set; } - } - - public class CubeConstructorTestEntity - { - public int Id { get; set; } - public double Coord1 { get; set; } - public double Coord2 { get; set; } - public double[] Coordinates { get; set; } = null!; - public double[] LowerLeft { get; set; } = null!; - public double[] UpperRight { get; set; } = null!; - public NpgsqlCube BaseCube { get; set; } - } - - public class CubeContext(DbContextOptions options) : PoolableDbContext(options) - { - public DbSet CubeTestEntities { get; set; } - public DbSet CubeConstructorTestEntities { get; set; } - - protected override void OnModelCreating(ModelBuilder builder) - => builder.HasPostgresExtension("cube"); - - public static async Task SeedAsync(CubeContext context) - { - context.CubeTestEntities.AddRange( - new CubeTestEntity - { - Id = 1, - // 3D non-point cube with all different dimensions - Cube = new NpgsqlCube( - [1.0, 2.0, 3.0], - [4.0, 5.0, 6.0] - ), // (1,2,3),(4,5,6) - IndexArray = [0, 1] // Extract first two dimensions - }, - new CubeTestEntity - { - Id = 2, - // 3D point cube with all different dimensions - Cube = new NpgsqlCube( - [7.0, 8.0, 9.0] - ), // (7,8,9) - IndexArray = [2, 1, 0] // Reverse order - }, - new CubeTestEntity - { - Id = 3, - // Non-overlapping cube with all different dimensions (far from others) - Cube = new NpgsqlCube( - [20.0, 30.0, 40.0], - [25.0, 35.0, 45.0] - ), // (20,30,40),(25,35,45) - IndexArray = [1, 2] // Extract middle and last dimensions - }, - new CubeTestEntity - { - Id = 4, - // Far away cube for distance tests, all different dimensions - Cube = new NpgsqlCube( - [100.0, 200.0, 300.0], - [400.0, 500.0, 600.0] - ), // (100,200,300),(400,500,600) - IndexArray = [0, 0, 1] // Duplicate indices - }, - new CubeTestEntity - { - Id = 5, - // 1D cube (single point) - Cube = new NpgsqlCube(2.5), // (2.5) - IndexArray = [0] // Single index - }, - new CubeTestEntity - { - Id = 6, - // Cube with negative coordinates for testing - Cube = new NpgsqlCube( - [-5.0, -10.0, -15.0], - [-2.0, -7.0, -12.0] - ), // (-5,-10,-15),(-2,-7,-12) - IndexArray = [] // Empty array - }); - - context.CubeConstructorTestEntities.AddRange( - new CubeConstructorTestEntity - { - Id = 1, - Coord1 = 10.0, - Coord2 = 20.0, - Coordinates = [10.0, 20.0, 30.0], - LowerLeft = [1.0, 2.0, 3.0], - UpperRight = [4.0, 5.0, 6.0], - BaseCube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]) - }, - new CubeConstructorTestEntity - { - Id = 2, - Coord1 = 40.0, - Coord2 = 50.0, - Coordinates = [40.0, 50.0, 60.0], - LowerLeft = [7.0, 8.0, 9.0], - UpperRight = [10.0, 11.0, 12.0], - BaseCube = new NpgsqlCube([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]) - }); - - await context.SaveChangesAsync(); - } - } - - #endregion - - #region Helpers - - protected CubeContext CreateContext() - => Fixture.CreateContext(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - #endregion -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsNpgsqlTest.cs deleted file mode 100644 index e079200485..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,308 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class EnumTranslationsNpgsqlTest : EnumTranslationsTestBase -{ - public EnumTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Equality - - public override async Task Equality_to_constant() - { - await base.Equality_to_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Enum" = 0 -"""); - } - - public override async Task Equality_to_parameter() - { - await base.Equality_to_parameter(); - - AssertSql( - """ -@basicEnum='0' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Enum" = @basicEnum -"""); - } - - public override async Task Equality_nullable_enum_to_constant() - { - await base.Equality_nullable_enum_to_constant(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."Enum" = 0 -"""); - } - - public override async Task Equality_nullable_enum_to_parameter() - { - await base.Equality_nullable_enum_to_parameter(); - - AssertSql( - """ -@basicEnum='0' - -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."Enum" = @basicEnum -"""); - } - - public override async Task Equality_nullable_enum_to_null_constant() - { - await base.Equality_nullable_enum_to_null_constant(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."Enum" IS NULL -"""); - } - - public override async Task Equality_nullable_enum_to_null_parameter() - { - await base.Equality_nullable_enum_to_null_parameter(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."Enum" IS NULL -"""); - } - - public override async Task Equality_nullable_enum_to_nullable_parameter() - { - await base.Equality_nullable_enum_to_nullable_parameter(); - - AssertSql( - """ -@basicEnum='0' (Nullable = true) - -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."Enum" = @basicEnum -"""); - } - - #endregion Equality - - public override async Task Bitwise_and_enum_constant() - { - await base.Bitwise_and_enum_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 1 > 0 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 1 = 1 -"""); - } - - public override async Task Bitwise_and_integral_constant() - { - await base.Bitwise_and_integral_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 8 = 8 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum"::bigint & 8 = 8 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum"::smallint & 8 = 8 -"""); - } - - public override async Task Bitwise_and_nullable_enum_with_constant() - { - await base.Bitwise_and_nullable_enum_with_constant(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."FlagsEnum" & 8 > 0 -"""); - } - - public override async Task Where_bitwise_and_nullable_enum_with_null_constant() - { - await base.Where_bitwise_and_nullable_enum_with_null_constant(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."FlagsEnum" & NULL > 0 -"""); - } - - public override async Task Where_bitwise_and_nullable_enum_with_non_nullable_parameter() - { - await base.Where_bitwise_and_nullable_enum_with_non_nullable_parameter(); - - AssertSql( - """ -@flagsEnum='8' - -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."FlagsEnum" & @flagsEnum > 0 -"""); - } - - public override async Task Where_bitwise_and_nullable_enum_with_nullable_parameter() - { - await base.Where_bitwise_and_nullable_enum_with_nullable_parameter(); - - AssertSql( - """ -@flagsEnum='8' (Nullable = true) - -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."FlagsEnum" & @flagsEnum > 0 -""", - // - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."FlagsEnum" & NULL > 0 -"""); - } - - public override async Task Bitwise_or() - { - await base.Bitwise_or(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" | 8 > 0 -"""); - } - - public override async Task Bitwise_projects_values_in_select() - { - await base.Bitwise_projects_values_in_select(); - - AssertSql( - """ -SELECT b."FlagsEnum" & 8 = 8 AS "BitwiseTrue", b."FlagsEnum" & 8 = 4 AS "BitwiseFalse", b."FlagsEnum" & 8 AS "BitwiseValue" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 8 = 8 -LIMIT 1 -"""); - } - - public override async Task HasFlag() - { - await base.HasFlag(); - -AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 8 = 8 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 12 = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 8 = 8 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 8 = 8 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE 8 & b."FlagsEnum" = b."FlagsEnum" -""", - // - """ -SELECT b."FlagsEnum" & 8 = 8 AS "hasFlagTrue", b."FlagsEnum" & 4 = 4 AS "hasFlagFalse" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & 8 = 8 -LIMIT 1 -"""); - } - - public override async Task HasFlag_with_non_nullable_parameter() - { - await base.HasFlag_with_non_nullable_parameter(); - - AssertSql( - """ -@flagsEnum='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & @flagsEnum = @flagsEnum -"""); - } - - public override async Task HasFlag_with_nullable_parameter() - { - await base.HasFlag_with_nullable_parameter(); - - AssertSql( - """ -@flagsEnum='8' (Nullable = true) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."FlagsEnum" & @flagsEnum = @flagsEnum -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs deleted file mode 100644 index 60ea6ead02..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/GuidTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; - -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class GuidTranslationsNpgsqlTest : GuidTranslationsTestBase -{ - // ReSharper disable once UnusedParameter.Local - public GuidTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task New_with_constant() - { - await base.New_with_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Guid" = 'df36f493-463f-4123-83f9-6b135deeb7ba' -"""); - } - - public override async Task New_with_parameter() - { - await base.New_with_parameter(); - - AssertSql( - """ -@p='df36f493-463f-4123-83f9-6b135deeb7ba' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Guid" = @p -"""); - } - - public override async Task ToString_projection() - { - await base.ToString_projection(); - - AssertSql( - """ -SELECT b."Guid"::text -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task NewGuid() - { - await base.NewGuid(); - - if (TestEnvironment.PostgresVersion >= new Version(13, 0)) - { - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE gen_random_uuid() <> '00000000-0000-0000-0000-000000000000' -"""); - } - else - { - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE uuid_generate_v4() <> '00000000-0000-0000-0000-000000000000' -"""); - } - } - - [ConditionalFact] - public virtual async Task CreateVersion7() - { - await AssertQuery( - ss => ss.Set() - .Where(od => Guid.CreateVersion7() != default)); - - if (TestEnvironment.PostgresVersion >= new Version(18, 0)) - { - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE uuidv7() <> '00000000-0000-0000-0000-000000000000' -"""); - } - else - { - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -"""); - } - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs deleted file mode 100644 index 36a6960e04..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/MathTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,761 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class MathTranslationsNpgsqlTest : MathTranslationsTestBase -{ - public MathTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Abs_decimal() - { - await base.Abs_decimal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE abs(b."Decimal") = 9.5 -"""); - } - - public override async Task Abs_int() - { - await base.Abs_int(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE abs(b."Int") = 9 -"""); - } - - public override async Task Abs_double() - { - await base.Abs_double(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE abs(b."Double") = 9.5 -"""); - } - - public override async Task Abs_float() - { - await base.Abs_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE abs(b."Float")::double precision = 9.5 -"""); - } - - public override async Task Ceiling() - { - await base.Ceiling(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE ceiling(b."Double") = 9.0 -"""); - } - - public override async Task Ceiling_float() - { - await base.Ceiling_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE ceiling(b."Float") = 9 -"""); - } - - public override async Task Floor_decimal() - { - await base.Floor_decimal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(b."Decimal") = 8.0 -"""); - } - - public override async Task Floor_double() - { - await base.Floor_double(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(b."Double") = 8.0 -"""); - } - - public override async Task Floor_float() - { - await base.Floor_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(b."Float") = 8 -"""); - } - - public override async Task Power() - { - await base.Power(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE power(b."Int"::double precision, 2.0) = 64.0 -"""); - } - - public override async Task Power_float() - { - await base.Power_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE power(b."Float", 2) > 73 AND power(b."Float", 2) < 74 -"""); - } - - public override async Task Round_decimal() - { - await base.Round_decimal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE round(b."Decimal") = 9.0 -""", - // - """ -SELECT round(b."Decimal") -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Round_double() - { - await base.Round_double(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE round(b."Double") = 9.0 -""", - // - """ -SELECT round(b."Double") -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Round_float() - { - await base.Round_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE round(b."Float")::real = 9 -""", - // - """ -SELECT round(b."Float")::real -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Round_with_digits_decimal() - { - await base.Round_with_digits_decimal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE round(b."Decimal", 1) = 255.1 -"""); - } - - // PostgreSQL only has round(v, s) over numeric, may be possible to cast back and forth though - public override Task Round_with_digits_double() - => AssertTranslationFailed(() => base.Round_with_digits_double()); - - // PostgreSQL only has round(v, s) over numeric, may be possible to cast back and forth though - public override Task Round_with_digits_float() - => AssertTranslationFailed(() => base.Round_with_digits_float()); - - public override async Task Truncate_decimal() - { - await base.Truncate_decimal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE trunc(b."Decimal") = 8.0 -""", - // - """ -SELECT trunc(b."Decimal") -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Truncate_double() - { - await base.Truncate_double(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE trunc(b."Double") = 8.0 -""", - // - """ -SELECT trunc(b."Double") -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Truncate_float() - { - await base.Truncate_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE trunc(b."Float")::real = 8 -""", - // - """ -SELECT trunc(b."Float")::real -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Truncate_project_and_order_by_it_twice() - { - await base.Truncate_project_and_order_by_it_twice(); - - AssertSql( - """ -SELECT trunc(b."Double") AS "A" -FROM "BasicTypesEntities" AS b -ORDER BY trunc(b."Double") NULLS FIRST -"""); - } - - // issue #16038 - // AssertSql( - // @"SELECT ROUND(CAST([o].[OrderID] AS float), 0, 1) AS [A] - //FROM [Orders] AS [o] - //WHERE [o].[OrderID] < 10250 - //ORDER BY [A]"); - public override async Task Truncate_project_and_order_by_it_twice2() - { - await base.Truncate_project_and_order_by_it_twice2(); - - AssertSql( - """ -SELECT trunc(b."Double") AS "A" -FROM "BasicTypesEntities" AS b -ORDER BY trunc(b."Double") DESC NULLS LAST -"""); - } - - // issue #16038 - // AssertSql( - // @"SELECT ROUND(CAST([o].[OrderID] AS float), 0, 1) AS [A] - //FROM [Orders] AS [o] - //WHERE [o].[OrderID] < 10250 - //ORDER BY [A] DESC"); - public override async Task Truncate_project_and_order_by_it_twice3() - { - await base.Truncate_project_and_order_by_it_twice3(); - - AssertSql( - """ -SELECT trunc(b."Double") AS "A" -FROM "BasicTypesEntities" AS b -ORDER BY trunc(b."Double") DESC NULLS LAST -"""); - } - - public override async Task Exp() - { - await base.Exp(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE exp(b."Double") > 1.0 -"""); - } - - public override async Task Exp_float() - { - await base.Exp_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE exp(b."Float") > 1 -"""); - } - - public override async Task Log() - { - await base.Log(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Double" > 0.0 AND ln(b."Double") <> 0.0 -"""); - } - - public override async Task Log_float() - { - await base.Log_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Float" > 0 AND ln(b."Float") <> 0 -"""); - } - - // PostgreSQL only has log(x, base) over numeric, may be possible to cast back and forth though - public override Task Log_with_newBase() - => AssertTranslationFailed(() => base.Log_with_newBase()); - - // PostgreSQL only has log(x, base) over numeric, may be possible to cast back and forth though - public override Task Log_with_newBase_float() - => AssertTranslationFailed(() => base.Log_with_newBase_float()); - - public override async Task Log10() - { - await base.Log10(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Double" > 0.0 AND log(b."Double") <> 0.0 -"""); - } - - public override async Task Log10_float() - { - await base.Log10_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Float" > 0 AND log(b."Float") <> 0 -"""); - } - - public override async Task Log2() - => await AssertTranslationFailed(() => base.Log2()); - - public override async Task Sqrt() - { - await base.Sqrt(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Double" > 0.0 AND sqrt(b."Double") > 0.0 -"""); - } - - public override async Task Sqrt_float() - { - await base.Sqrt_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Float" > 0 AND sqrt(b."Float") > 0 -"""); - } - - public override async Task Sign() - { - await base.Sign(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE sign(b."Double")::int > 0 -"""); - } - - public override async Task Sign_float() - { - await base.Sign_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE sign(b."Float")::int > 0 -"""); - } - - public override async Task Max() - { - await base.Max(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE GREATEST(b."Int", b."Short" - 3) = b."Int" -"""); - } - - public override async Task Max_nested() - { - await base.Max_nested(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE GREATEST(b."Short" - 3, b."Int", 1) = b."Int" -"""); - } - - public override async Task Max_nested_twice() - { - await base.Max_nested_twice(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE GREATEST(1, b."Int", 2, b."Short" - 3) = b."Int" -"""); - } - - public override async Task Min() - { - await base.Min(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE LEAST(b."Int", b."Short" + 3) = b."Int" -"""); - } - - public override async Task Min_nested() - { - await base.Min_nested(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE LEAST(b."Short" + 3, b."Int", 99999) = b."Int" -"""); - } - - public override async Task Min_nested_twice() - { - await base.Min_nested_twice(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE LEAST(99999, b."Int", 99998, b."Short" + 3) = b."Int" -"""); - } - - public override async Task Degrees() - { - await base.Degrees(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE degrees(b."Double") > 0.0 -"""); - } - - public override async Task Degrees_float() - { - await base.Degrees_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE degrees(b."Float") > 0 -"""); - } - - public override async Task Radians() - { - await base.Radians(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE radians(b."Double") > 0.0 -"""); - } - - public override async Task Radians_float() - { - await base.Radians_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE radians(b."Float") > 0 -"""); - } - - #region Trigonometry - - public override async Task Acos() - { - await base.Acos(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Double" >= -1.0 AND b."Double" <= 1.0 AND acos(b."Double") > 1.0 -"""); - } - - public override async Task Acos_float() - { - await base.Acos_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Float" >= -1 AND b."Float" <= 1 AND acos(b."Float") > 0 -"""); - } - - public override async Task Acosh() - => await AssertTranslationFailed(() => base.Acosh()); - - public override async Task Asin() - { - await base.Asin(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Double" >= -1.0 AND b."Double" <= 1.0 AND asin(b."Double") > -1.7976931348623157E+308 -"""); - } - - public override async Task Asin_float() - { - await base.Asin_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Float" >= -1 AND b."Float" <= 1 AND asin(b."Float")::double precision > -1.7976931348623157E+308 -"""); - } - - public override async Task Asinh() - => await AssertTranslationFailed(() => base.Asinh()); - - public override async Task Atan() - { - await base.Atan(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE atan(b."Double") > 0.0 -"""); - } - - public override async Task Atan_float() - { - await base.Atan_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE atan(b."Float") > 0 -"""); - } - - public override async Task Atanh() - => await AssertTranslationFailed(() => base.Atanh()); - - public override async Task Atan2() - { - await base.Atan2(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE atan2(b."Double", 1.0) > 0.0 -"""); - } - - public override async Task Atan2_float() - { - await base.Atan2_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE atan2(b."Float", 1) > 0 -"""); - } - - public override async Task Cos() - { - await base.Cos(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE cos(b."Double") > 0.0 -"""); - } - - public override async Task Cos_float() - { - await base.Cos_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE cos(b."Float") > 0 -"""); - } - - public override async Task Cosh() - => await AssertTranslationFailed(() => base.Cosh()); - - public override async Task Sin() - { - await base.Sin(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE sin(b."Double") > 0.0 -"""); - } - - public override async Task Sin_float() - { - await base.Sin_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE sin(b."Float") > 0 -"""); - } - - public override async Task Sinh() - => await AssertTranslationFailed(() => base.Sinh()); - - public override async Task Tan() - { - await base.Tan(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE tan(b."Double") > 0.0 -"""); - } - - public override async Task Tan_float() - { - await base.Tan_float(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE tan(b."Float") > 0 -"""); - } - - public override async Task Tanh() - => await AssertTranslationFailed(() => base.Tanh()); - - #endregion Trigonometry - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs deleted file mode 100644 index 437f31dbde..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/MiscellaneousTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,399 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; - -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class MiscellaneousTranslationsNpgsqlTest : MiscellaneousTranslationsRelationalTestBase -{ - public MiscellaneousTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Random - - public override async Task Random_on_EF_Functions() - { - await base.Random_on_EF_Functions(); - - AssertSql( - """ -SELECT count(*)::int -FROM "BasicTypesEntities" AS b -WHERE random() >= 0.0 AND random() < 1.0 -"""); - } - - public override async Task Random_Shared_Next_with_no_args() - { - await base.Random_Shared_Next_with_no_args(); - - AssertSql(); - } - - public override async Task Random_Shared_Next_with_one_arg() - { - await base.Random_Shared_Next_with_one_arg(); - - AssertSql(); - } - - public override async Task Random_Shared_Next_with_two_args() - { - await base.Random_Shared_Next_with_two_args(); - - AssertSql(); - } - - public override async Task Random_new_Next_with_no_args() - { - await base.Random_new_Next_with_no_args(); - - AssertSql(); - } - - public override async Task Random_new_Next_with_one_arg() - { - await base.Random_new_Next_with_one_arg(); - - AssertSql(); - } - - public override async Task Random_new_Next_with_two_args() - { - await base.Random_new_Next_with_two_args(); - - AssertSql(); - } - - #endregion Random - - #region Convert - - // These tests convert (among other things) to and from boolean, which PostgreSQL - // does not support (https://github.com/dotnet/efcore/issues/19606) - - public override async Task Convert_ToBoolean() - { - var exception = await Assert.ThrowsAsync(() => base.Convert_ToBoolean()); - Assert.Equal("42846", exception.SqlState); - } - - public override async Task Convert_ToByte() - { - var exception = await Assert.ThrowsAsync(() => base.Convert_ToByte()); - Assert.Equal("42846", exception.SqlState); - } - - public override async Task Convert_ToDecimal() - { - var exception = await Assert.ThrowsAsync(() => base.Convert_ToDecimal()); - Assert.Equal("42846", exception.SqlState); - } - - public override async Task Convert_ToDouble() - { - var exception = await Assert.ThrowsAsync(() => base.Convert_ToDouble()); - Assert.Equal("42846", exception.SqlState); - } - - public override async Task Convert_ToInt16() - { - var exception = await Assert.ThrowsAsync(() => base.Convert_ToInt16()); - Assert.Equal("42846", exception.SqlState); - } - - public override async Task Convert_ToInt32() - { - await base.Convert_ToInt32(); - -AssertSql( -""" -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Bool"::int = 1 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Byte"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Decimal"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Double"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Float"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Short"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Long"::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::text::int = 12 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::int = 12 -"""); - } - - public override async Task Convert_ToInt64() - { - var exception = await Assert.ThrowsAsync(() => base.Convert_ToInt64()); - Assert.Equal("42846", exception.SqlState); - } - - // Convert on DateTime not yet supported - public override Task Convert_ToString() - => AssertTranslationFailed(() => base.Convert_ToString()); - - #endregion Convert - - #region Compare - - public override async Task Int_Compare_to_simple_zero() - { - await base.Int_Compare_to_simple_zero(); - -AssertSql( - """ -@orderId='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = @orderId -""", - // - """ -@orderId='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" <> @orderId -""", - // - """ -@orderId='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" > @orderId -""", - // - """ -@orderId='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" <= @orderId -""", - // - """ -@orderId='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" > @orderId -""", - // - """ -@orderId='8' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" <= @orderId -"""); - } - - public override async Task DateTime_Compare_to_simple_zero(bool compareTo) - { - // The base test implementation uses an Unspecified DateTime, which isn't supported with PostgreSQL timestamptz - var dateTime = new DateTime(1998, 5, 4, 15, 30, 10, DateTimeKind.Utc); - - if (compareTo) - { - await AssertQuery( - ss => ss.Set().Where(c => c.DateTime.CompareTo(dateTime) == 0)); - - await AssertQuery( - ss => ss.Set().Where(c => 0 != c.DateTime.CompareTo(dateTime))); - - await AssertQuery( - ss => ss.Set().Where(c => c.DateTime.CompareTo(dateTime) > 0)); - - await AssertQuery( - ss => ss.Set().Where(c => 0 >= c.DateTime.CompareTo(dateTime))); - - await AssertQuery( - ss => ss.Set().Where(c => 0 < c.DateTime.CompareTo(dateTime))); - - await AssertQuery( - ss => ss.Set().Where(c => c.DateTime.CompareTo(dateTime) <= 0)); - } - else - { - await AssertQuery( - ss => ss.Set().Where(c => DateTime.Compare(c.DateTime, dateTime) == 0)); - - await AssertQuery( - ss => ss.Set().Where(c => 0 != DateTime.Compare(c.DateTime, dateTime))); - - await AssertQuery( - ss => ss.Set().Where(c => DateTime.Compare(c.DateTime, dateTime) > 0)); - - await AssertQuery( - ss => ss.Set().Where(c => 0 >= DateTime.Compare(c.DateTime, dateTime))); - - await AssertQuery( - ss => ss.Set().Where(c => 0 < DateTime.Compare(c.DateTime, dateTime))); - - await AssertQuery( - ss => ss.Set().Where(c => DateTime.Compare(c.DateTime, dateTime) <= 0)); - } - -AssertSql( -""" -@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" = @dateTime -""", - // - """ -@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" <> @dateTime -""", - // - """ -@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" > @dateTime -""", - // - """ -@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" <= @dateTime -""", - // - """ -@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" > @dateTime -""", - // - """ -@dateTime='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" <= @dateTime -"""); - } - - public override async Task TimeSpan_Compare_to_simple_zero(bool compareTo) - { - await base.TimeSpan_Compare_to_simple_zero(compareTo); - - AssertSql( - """ -@timeSpan='01:02:03' (DbType = Object) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan" = @timeSpan -""", - // - """ -@timeSpan='01:02:03' (DbType = Object) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan" <> @timeSpan -""", - // - """ -@timeSpan='01:02:03' (DbType = Object) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan" > @timeSpan -""", - // - """ -@timeSpan='01:02:03' (DbType = Object) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan" <= @timeSpan -""", - // - """ -@timeSpan='01:02:03' (DbType = Object) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan" > @timeSpan -""", - // - """ -@timeSpan='01:02:03' (DbType = Object) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan" <= @timeSpan -"""); - } - - #endregion Compare - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NetworkTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NetworkTranslationsNpgsqlTest.cs deleted file mode 100644 index 99bddfe487..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/NetworkTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,2161 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Net; -using System.Net.NetworkInformation; - -// ReSharper disable ConvertToConstant.Local - -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -/// -/// Provides unit tests for network address operator and function translations. -/// -/// -/// See: https://www.postgresql.org/docs/current/static/functions-net.html -/// -public class NetworkTranslationsNpgsqlTest : IClassFixture -{ - private NetworkAddressQueryNpgsqlFixture Fixture { get; } - - // ReSharper disable once UnusedParameter.Local - public NetworkTranslationsNpgsqlTest(NetworkAddressQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region BugTests - - [Fact] - public void Demonstrate_ValueTypeParametersAreDuplicated() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrEqual(x.IPNetwork, cidr)) - .Select(x => x.IPNetwork.Equals(cidr)) - .ToArray(); - - AssertSql( - """ -@cidr='0.0.0.0/0' (DbType = Object) -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."IPNetwork" = @cidr -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" >>= @p -"""); - } - - #endregion - - #region ParseTests - - [Fact] - public void IPAddress_inet_parse_column() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => x.Inet.Equals(IPAddress.Parse(x.TextInet))); - - Assert.Equal(9, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" = n."TextInet"::inet -"""); - } - - [Fact] - public void PhysicalAddress_macaddr_parse_column() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => x.Macaddr.Equals(PhysicalAddress.Parse(x.TextMacaddr))); - - Assert.Equal(9, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr" = n."TextMacaddr"::macaddr -"""); - } - - [Fact] - public void IPAddress_inet_parse_literal() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => x.Inet.Equals(IPAddress.Parse("192.168.1.2"))); - - Assert.Equal(1, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" = INET '192.168.1.2' -"""); - } - - [Fact] - public void PhysicalAddress_macaddr_parse_literal() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => x.Macaddr.Equals(PhysicalAddress.Parse("12-34-56-00-00-02"))); - - Assert.Equal(1, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr" = MACADDR '123456000002' -"""); - } - - [Fact] - public void IPAddress_inet_parse_parameter() - { - using var context = CreateContext(); - var inet = "192.168.1.2"; - var count = context.NetTestEntities.Count(x => x.Inet.Equals(IPAddress.Parse(inet))); - - Assert.Equal(1, count); - } - - [Fact] - public void PhysicalAddress_macaddr_parse_parameter() - { - using var context = CreateContext(); - var macaddr = "12-34-56-00-00-01"; - var count = context.NetTestEntities.Count(x => x.Macaddr.Equals(PhysicalAddress.Parse(macaddr))); - - Assert.Equal(1, count); - } - - #endregion - - #region RelationalOperatorTests - - [Fact] - public void LessThan_IPAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.LessThan(x.Inet, IPAddress.Parse("192.168.1.7"))); - - Assert.Equal(6, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" < INET '192.168.1.7' -"""); - } - - [Fact] - public void LessThan_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.LessThan(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" < @p -"""); - } - - [Fact] - public void LessThan_PhysicalAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.LessThan(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); - - Assert.Equal(6, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr" < MACADDR '123456000007' -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void LessThan_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.LessThan(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); - - Assert.Equal(6, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr8" < MACADDR8 '08002B0102030407' -"""); - } - - [Fact] - public void LessThanOrEqual_IPAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.LessThanOrEqual(x.Inet, IPAddress.Parse("192.168.1.7"))); - - Assert.Equal(7, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" <= INET '192.168.1.7' -"""); - } - - [Fact] - public void LessThanOrEqual_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.LessThanOrEqual(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" <= @p -"""); - } - - [Fact] - public void LessThanOrEqual_PhysicalAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.LessThanOrEqual(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); - - Assert.Equal(7, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr" <= MACADDR '123456000007' -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void LessThanOrEqual_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count( - x => EF.Functions.LessThanOrEqual(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); - - Assert.Equal(7, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr8" <= MACADDR8 '08002B0102030407' -"""); - } - - [Fact] - public void GreaterThanOrEqual_IPAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.GreaterThanOrEqual(x.Inet, IPAddress.Parse("192.168.1.7"))); - - Assert.Equal(3, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" >= INET '192.168.1.7' -"""); - } - - [Fact] - public void GreaterThanOrEqual_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.GreaterThanOrEqual(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" >= @p -"""); - } - - [Fact] - public void GreaterThanOrEqual_PhysicalAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count( - x => EF.Functions.GreaterThanOrEqual(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); - - Assert.Equal(3, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr" >= MACADDR '123456000007' -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void GreaterThanOrEqual_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count( - x => EF.Functions.GreaterThanOrEqual(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); - - Assert.Equal(3, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr8" >= MACADDR8 '08002B0102030407' -"""); - } - - [Fact] - public void GreaterThan_IPAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.GreaterThan(x.Inet, IPAddress.Parse("192.168.1.7"))); - - Assert.Equal(2, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" > INET '192.168.1.7' -"""); - } - - [Fact] - public void GreaterThan_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.GreaterThan(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" > @p -"""); - } - - [Fact] - public void GreaterThan_PhysicalAddress() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count(x => EF.Functions.GreaterThan(x.Macaddr, PhysicalAddress.Parse("12-34-56-00-00-07"))); - - Assert.Equal(2, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr" > MACADDR '123456000007' -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void GreaterThan_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - var count = context.NetTestEntities.Count( - x => EF.Functions.GreaterThan(x.Macaddr8, PhysicalAddress.Parse("08-00-2B-01-02-03-04-07"))); - - Assert.Equal(2, count); - AssertSql( - """ -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Macaddr8" > MACADDR8 '08002B0102030407' -"""); - } - - #endregion - - #region ContainmentOperatorTests - - [Fact] - public void ContainedBy_IPAddress_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedBy(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" << @p -"""); - } - - [Fact] - public void ContainedBy_IPAddress_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedBy(x.Inet, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" << @p -"""); - } - - [Fact] - public void ContainedBy_IPNetwork_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedBy(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" << @p -"""); - } - - [Fact] - public void ContainedByOrEqual_IPAddress_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedByOrEqual(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" <<= @p -"""); - } - - [Fact] - public void ContainedByOrEqual_IPAddress_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedByOrEqual(x.Inet, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" <<= @p -"""); - } - - [Fact] - public void ContainedByOrEqual_IPNetwork_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedByOrEqual(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" <<= @p -"""); - } - - [Fact] - public void Contains_IPAddress_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.Contains(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" >> @p -"""); - } - - [Fact] - public void Contains_IPNetwork_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.Contains(x.IPNetwork, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" >> @p -"""); - } - - [Fact] - public void Contains_IPNetwork_and_IPNetworks() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.Contains(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" >> @p -"""); - } - - [Fact] - public void ContainsOrEqual_IPAddress_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrEqual(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" >>= @p -"""); - } - - [Fact] - public void ContainsOrEqual_IPNetwork_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrEqual(x.IPNetwork, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" >>= @p -"""); - } - - [Fact] - public void ContainsOrEqual_IPNetwork_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrEqual(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" >>= @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_IPAddress_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" && @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_IPAddress_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.Inet, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" && @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_IPNetwork_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.IPNetwork, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" && @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_IPNetwork_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."IPNetwork" && @p -"""); - } - - #endregion - - #region BitwiseOperatorTests - - [Fact] - public void BitwiseNot_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseNot(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT ~n."Inet" -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseNot_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseNot(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT ~n."IPNetwork" -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseNot_PhysicalAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseNot(x.Macaddr)) - .ToArray(); - - AssertSql( - """ -SELECT ~n."Macaddr" -FROM "NetTestEntities" AS n -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void BitwiseNot_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseNot(x.Macaddr8)) - .ToArray(); - - AssertSql( - """ -SELECT ~n."Macaddr8" -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseAnd_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - var count = context.NetTestEntities.Count(x => x.Inet == EF.Functions.BitwiseAnd(x.Inet, inet)); - - Assert.Equal(0, count); - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT count(*)::int -FROM "NetTestEntities" AS n -WHERE n."Inet" = n."Inet" & @p -"""); - } - - [Fact] - public void BitwiseAnd_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseAnd(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."IPNetwork" & @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseAnd_PhysicalAddress() - { - using var context = CreateContext(); - var macaddr = new PhysicalAddress(new byte[6]); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseAnd(x.Macaddr, macaddr)) - .ToArray(); - - AssertSql( - """ -@macaddr='000000000000' (DbType = Object) - -SELECT n."Macaddr" & @macaddr -FROM "NetTestEntities" AS n -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void BitwiseAnd_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseAnd(x.Macaddr8, x.Macaddr8)) - .ToArray(); - - AssertSql( - """ -SELECT n."Macaddr8" & n."Macaddr8" -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseOr_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseOr(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Inet" | @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseOr_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseOr(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."IPNetwork" | @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseOr_PhysicalAddress() - { - using var context = CreateContext(); - var macaddr = new PhysicalAddress(new byte[6]); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseOr(x.Macaddr, macaddr)) - .ToArray(); - - AssertSql( - """ -@macaddr='000000000000' (DbType = Object) - -SELECT n."Macaddr" | @macaddr -FROM "NetTestEntities" AS n -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void BitwiseOr_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseOr(x.Macaddr8, x.Macaddr8)) - .ToArray(); - - AssertSql( - """ -SELECT n."Macaddr8" | n."Macaddr8" -FROM "NetTestEntities" AS n -"""); - } - - #endregion - - #region ArithmeticOperatorTests - - [Fact] - public void Add_IPAddress_and_int() - { - using var context = CreateContext(); - var actual = context.NetTestEntities.Single(x => EF.Functions.Add(x.Inet, 1) == IPAddress.Parse("192.168.1.2")).Inet; - - Assert.Equal(actual, IPAddress.Parse("192.168.1.1")); - AssertSql( - """ -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" + 1 = INET '192.168.1.2' -LIMIT 2 -"""); - } - - [Fact] - public void Add_IPNetwork_and_int() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Add(x.IPNetwork, 1)) - .ToArray(); - - AssertSql( - """ -SELECT n."IPNetwork" + 1 -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Subtract_IPAddress_and_int() - { - using var context = CreateContext(); - var actual = context.NetTestEntities.Single(x => EF.Functions.Subtract(x.Inet, 1) == IPAddress.Parse("192.168.1.1")).Inet; - - Assert.Equal(actual, IPAddress.Parse("192.168.1.2")); - AssertSql( - """ -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" - 1 = INET '192.168.1.1' -LIMIT 2 -"""); - } - - [Fact] - public void Subtract_IPNetwork_and_int() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Subtract(x.IPNetwork, 1)) - .ToArray(); - - AssertSql( - """ -SELECT n."IPNetwork" - 1 -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Subtract_IPAddress_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Select(x => EF.Functions.Subtract(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Inet" - @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Subtract_IPNetwork_and_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.Subtract(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."IPNetwork" - @p -FROM "NetTestEntities" AS n -"""); - } - - #endregion - - #region FunctionTests - - [Fact] - public void Abbreviate_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Abbreviate(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT abbrev(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Abbreviate_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Abbreviate(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT abbrev(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Broadcast_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Broadcast(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT broadcast(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Broadcast_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Broadcast(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT broadcast(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Family_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Family(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT family(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Family_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Family(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT family(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Host_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Host(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT host(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Host_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Host(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT host(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void HostMask_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.HostMask(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT hostmask(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void HostMask_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.HostMask(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT hostmask(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void MaskLength_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.MaskLength(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT masklen(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void MaskLength_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.MaskLength(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT masklen(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Netmask_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Netmask(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT netmask(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Netmask_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Netmask(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT netmask(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Network_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Network(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT network(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Network_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Network(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT network(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void SetMaskLength_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.SetMaskLength(x.Inet, default)) - .ToArray(); - - AssertSql( - """ -SELECT set_masklen(n."Inet", 0) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void SetMaskLength_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.SetMaskLength(x.IPNetwork, default)) - .ToArray(); - - AssertSql( - """ -SELECT set_masklen(n."IPNetwork", 0) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Text_IPAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Text(x.Inet)) - .ToArray(); - - AssertSql( - """ -SELECT text(n."Inet") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Text_IPNetwork() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Text(x.IPNetwork)) - .ToArray(); - - AssertSql( - """ -SELECT text(n."IPNetwork") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void SameFamily_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Select(x => EF.Functions.SameFamily(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT inet_same_family(n."Inet", @p) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void SameFamily_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.SameFamily(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT inet_same_family(n."IPNetwork", @p) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Merge_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Select(x => EF.Functions.Merge(x.Inet, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT inet_merge(n."Inet", @p) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Merge_IPNetwork() - { - using var context = CreateContext(); - var cidr = new IPNetwork(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.Merge(x.IPNetwork, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT inet_merge(n."IPNetwork", @p) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Truncate_PhysicalAddress() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Truncate(x.Macaddr)) - .ToArray(); - - AssertSql( - """ -SELECT trunc(n."Macaddr") -FROM "NetTestEntities" AS n -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void Truncate_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Truncate(x.Macaddr8)) - .ToArray(); - - AssertSql( - """ -SELECT trunc(n."Macaddr8") -FROM "NetTestEntities" AS n -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void Set7BitMac8_PhysicalAddress_macaddr8() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Set7BitMac8(x.Macaddr8)) - .ToArray(); - - AssertSql( - """ -SELECT macaddr8_set7bit(n."Macaddr8") -FROM "NetTestEntities" AS n -"""); - } - - #endregion - - #region Obsolete (NpgsqlCidr) - -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - - [Fact] - public void LessThan_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.LessThan(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" < @p -"""); - } - - [Fact] - public void LessThanOrEqual_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.LessThanOrEqual(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" <= @p -"""); - } - - [Fact] - public void GreaterThanOrEqual_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.GreaterThanOrEqual(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" >= @p -"""); - } - - [Fact] - public void GreaterThan_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.GreaterThan(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" > @p -"""); - } - - [Fact] - public void ContainedBy_IPAddress_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedBy(x.Inet, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" << @p -"""); - } - - [Fact] - public void ContainedBy_NpgsqlCidr_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedBy(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" << @p -"""); - } - - [Fact] - public void ContainedByOrEqual_IPAddress_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedByOrEqual(x.Inet, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" <<= @p -"""); - } - - [Fact] - public void ContainedByOrEqual_NpgsqlCidr_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainedByOrEqual(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" <<= @p -"""); - } - - [Fact] - public void Contains_NpgsqlCidr_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.Contains(x.Cidr, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" >> @p -"""); - } - - [Fact] - public void Contains_NpgsqlCidr_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.Contains(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" >> @p -"""); - } - - [Fact] - public void ContainsOrEqual_NpgsqlCidr_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrEqual(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" >>= @p -"""); - } - - [Fact] - public void ContainsOrEqual_NpgsqlCidr_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrEqual(x.Cidr, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" >>= @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_IPAddress_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.Inet, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Inet" && @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_NpgsqlCidr_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" && @p -"""); - } - - [Fact] - public void ContainsOrContainedBy_NpgsqlCidr_and_IPAddress() - { - using var context = CreateContext(); - var inet = IPAddress.Any; - _ = context.NetTestEntities - .Where(x => EF.Functions.ContainsOrContainedBy(x.Cidr, inet)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0' (DbType = Object) - -SELECT n."Id", n."Cidr", n."IPNetwork", n."Inet", n."Macaddr", n."Macaddr8", n."TextInet", n."TextMacaddr" -FROM "NetTestEntities" AS n -WHERE n."Cidr" && @p -"""); - } - - [Fact] - public void BitwiseAnd_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseAnd(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Cidr" & @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseOr_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseOr(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Cidr" | @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Subtract_NpgsqlCidr_and_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.Subtract(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT n."Cidr" - @p -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void SameFamily_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.SameFamily(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT inet_same_family(n."Cidr", @p) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Merge_NpgsqlCidr() - { - using var context = CreateContext(); - var cidr = new NpgsqlCidr(IPAddress.Any, default); - _ = context.NetTestEntities - .Select(x => EF.Functions.Merge(x.Cidr, cidr)) - .ToArray(); - - AssertSql( - """ -@p='0.0.0.0/0' (DbType = Object) - -SELECT inet_merge(n."Cidr", @p) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void BitwiseNot_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.BitwiseNot(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT ~n."Cidr" -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Add_NpgsqlCidr_and_int() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Add(x.Cidr, 1)) - .ToArray(); - - AssertSql( - """ -SELECT n."Cidr" + 1 -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Subtract_NpgsqlCidr_and_int() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Subtract(x.Cidr, 1)) - .ToArray(); - - AssertSql( - """ -SELECT n."Cidr" - 1 -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Abbreviate_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Abbreviate(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT abbrev(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Broadcast_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Broadcast(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT broadcast(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Family_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Family(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT family(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Host_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Host(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT host(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void HostMask_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.HostMask(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT hostmask(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void MaskLength_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.MaskLength(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT masklen(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Netmask_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Netmask(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT netmask(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Network_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Network(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT network(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void SetMaskLength_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.SetMaskLength(x.Cidr, default)) - .ToArray(); - - AssertSql( - """ -SELECT set_masklen(n."Cidr", 0) -FROM "NetTestEntities" AS n -"""); - } - - [Fact] - public void Text_NpgsqlCidr() - { - using var context = CreateContext(); - _ = context.NetTestEntities - .Select(x => EF.Functions.Text(x.Cidr)) - .ToArray(); - - AssertSql( - """ -SELECT text(n."Cidr") -FROM "NetTestEntities" AS n -"""); - } - -#pragma warning restore CS0618 - - #endregion Obsolete (NpgsqlCidr) - - #region Fixtures - - /// - /// Represents a fixture suitable for testing network address operators. - /// - public class NetworkAddressQueryNpgsqlFixture : SharedStoreFixtureBase - { - protected override string StoreName - => "NetworkQueryTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override Task SeedAsync(NetContext context) - => NetContext.SeedAsync(context); - } - - /// - /// Represents an entity suitable for testing network address operators. - /// - public class NetTestEntity - { - // ReSharper disable once UnusedMember.Global - /// - /// The primary key. - /// - [Key] - public int Id { get; set; } - - /// - /// The network address. - /// - public IPAddress Inet { get; set; } = null!; - - /// - /// The network address. - /// - public IPNetwork IPNetwork { get; set; } - - /// - /// The network address. - /// - [Obsolete("NpgsqlCidr is obsolete, replaced by .NET IPNetwork")] - public NpgsqlCidr Cidr { get; set; } - - /// - /// The MAC address. - /// - public PhysicalAddress Macaddr { get; set; } = null!; - - /// - /// The MAC address. - /// - [Column(TypeName = "macaddr8")] - public PhysicalAddress Macaddr8 { get; set; } = null!; - - /// - /// The text form of . - /// - public string TextInet { get; set; } = null!; - - /// - /// The text form of . - /// - public string TextMacaddr { get; set; } = null!; - } - - /// - /// Represents a database suitable for testing network address operators. - /// - public class NetContext : PoolableDbContext - { - /// - /// Represents a set of entities with properties. - /// - public DbSet NetTestEntities { get; set; } - - /// - /// Initializes a . - /// - /// - /// The options to be used for configuration. - /// - public NetContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - if (TestEnvironment.PostgresVersion < new Version(10, 0)) - { - modelBuilder.Entity().Ignore(x => x.Macaddr8); - } - - base.OnModelCreating(modelBuilder); - } - - public static async Task SeedAsync(NetContext context) - { - for (var i = 1; i <= 9; i++) - { - var ip = IPAddress.Parse("192.168.1." + i); - var macaddr = PhysicalAddress.Parse("12-34-56-00-00-0" + i); - var macaddr8 = PhysicalAddress.Parse("08-00-2B-01-02-03-04-0" + i); - context.NetTestEntities.Add( - new NetTestEntity - { - Id = i, - Inet = ip, -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - Cidr = new NpgsqlCidr(IPAddress.Parse("192.168.1.0"), 24), -#pragma warning restore CS0618 - IPNetwork = new IPNetwork(IPAddress.Parse("192.168.1.0"), 24), - Macaddr = macaddr, - Macaddr8 = macaddr8, - TextInet = ip.ToString(), - TextMacaddr = macaddr.ToString() - }); - } - - await context.SaveChangesAsync(); - } - } - - #endregion - - #region Helpers - - protected NetContext CreateContext() - => Fixture.CreateContext(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - #endregion -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/ArithmeticOperatorTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/ArithmeticOperatorTranslationsNpgsqlTest.cs deleted file mode 100644 index 2e6b37b066..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/ArithmeticOperatorTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; - -public class ArithmeticOperatorTranslationsNpgsqlTest : ArithmeticOperatorTranslationsTestBase -{ - public ArithmeticOperatorTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Add() - { - await base.Add(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" + 2 = 10 -"""); - } - - public override async Task Subtract() - { - await base.Subtract(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" - 3 = 5 -"""); - } - - public override async Task Multiply() - { - await base.Multiply(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" * 2 = 16 -"""); - } - - public override async Task Modulo() - { - await base.Modulo(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" % 3 = 2 -"""); - } - - public override async Task Minus() - { - await base.Minus(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE -b."Int" = -8 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsNpgsqlTest.cs deleted file mode 100644 index a74825c27a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,205 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; - -public class BitwiseOperatorTranslationsNpgsqlTest : BitwiseOperatorTranslationsTestBase -{ - public BitwiseOperatorTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Or() - { - await base.Or(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::bigint | b."Long" = 7 -""", - // - """ -SELECT b."Int"::bigint | b."Long" -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Or_over_boolean() - { - await base.Or_over_boolean(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 12 OR b."String" = 'Seattle' -""", - // - """ -SELECT b."Int" = 12 OR b."String" = 'Seattle' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Or_multiple() - { - await base.Or_multiple(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."Int" | b."Short" AS bigint) | b."Long" = 7 -"""); - } - - public override async Task And() - { - await base.And(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" & b."Short" = 2 -""", - // - """ -SELECT b."Int" & b."Short" -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task And_over_boolean() - { - await base.And_over_boolean(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 8 AND b."String" = 'Seattle' -""", - // - """ -SELECT b."Int" = 8 AND b."String" = 'Seattle' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Xor() - { - await base.Xor(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE (b."Int" # b."Short") = 1 -""", - // - """ -SELECT b."Int" # b."Short" -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Xor_over_boolean() - { - await base.Xor_over_boolean(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE (b."Int" = b."Short") <> (b."String" = 'Seattle') -"""); - } - - public override async Task Complement() - { - await base.Complement(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE ~b."Int" = -9 -"""); - } - - public override async Task And_or_over_boolean() - { - await base.And_or_over_boolean(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE (b."Int" = 12 AND b."Short" = 12) OR b."String" = 'Seattle' -"""); - } - - public override async Task Or_with_logical_or() - { - await base.Or_with_logical_or(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 12 OR b."Short" = 12 OR b."String" = 'Seattle' -"""); - } - - public override async Task And_with_logical_and() - { - await base.And_with_logical_and(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 8 AND b."Short" = 8 AND b."String" = 'Seattle' -"""); - } - - public override async Task Or_with_logical_and() - { - await base.Or_with_logical_and(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE (b."Int" = 8 OR b."Short" = 9) AND b."String" = 'Seattle' -"""); - } - - public override async Task And_with_logical_or() - { - await base.And_with_logical_or(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE (b."Int" = 12 AND b."Short" = 12) OR b."String" = 'Seattle' -"""); - } - - public override Task Left_shift() - => AssertTranslationFailed(() => base.Left_shift()); - - public override Task Right_shift() - => AssertTranslationFailed(() => base.Right_shift()); - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/ComparisonOperatorTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/ComparisonOperatorTranslationsNpgsqlTest.cs deleted file mode 100644 index b61a1fceaf..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/ComparisonOperatorTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; - -public class ComparisonOperatorTranslationsNpgsqlTest : ComparisonOperatorTranslationsTestBase -{ - public ComparisonOperatorTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Equal() - { - await base.Equal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 8 -"""); - } - - public override async Task NotEqual() - { - await base.NotEqual(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" <> 8 -"""); - } - - public override async Task GreaterThan() - { - await base.GreaterThan(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" > 8 -"""); - } - - public override async Task GreaterThanOrEqual() - { - await base.GreaterThanOrEqual(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" >= 8 -"""); - } - - public override async Task LessThan() - { - await base.LessThan(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" < 8 -"""); - } - - public override async Task LessThanOrEqual() - { - await base.LessThanOrEqual(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" <= 8 -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/LogicalOperatorTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/LogicalOperatorTranslationsNpgsqlTest.cs deleted file mode 100644 index 3373a0e644..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/LogicalOperatorTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; - -public class LogicalOperatorTranslationsNpgsqlTest : LogicalOperatorTranslationsTestBase -{ - public LogicalOperatorTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task And() - { - await base.And(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 8 AND b."String" = 'Seattle' -"""); - } - - public override async Task And_with_bool_property() - { - await base.And_with_bool_property(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Bool" AND b."String" = 'Seattle' -"""); - } - - public override async Task Or() - { - await base.Or(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" = 999 OR b."String" = 'Seattle' -"""); - } - - public override async Task Or_with_bool_property() - { - await base.Or_with_bool_property(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Bool" OR b."String" = 'Seattle' -"""); - } - - public override async Task Not() - { - await base.Not(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int" <> 999 -"""); - } - - public override async Task Not_with_bool_property() - { - await base.Not_with_bool_property(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE NOT (b."Bool") -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs deleted file mode 100644 index 2522f98aca..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Operators; - -public class MiscellaneousOperatorTranslationsNpgsqlTest : MiscellaneousOperatorTranslationsTestBase -{ - public MiscellaneousOperatorTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Conditional() - { - await base.Conditional(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CASE - WHEN b."Int" = 8 THEN b."String" - ELSE 'Foo' -END = 'Seattle' -"""); - } - - public override async Task Coalesce() - { - await base.Coalesce(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE COALESCE(n."String", 'Unknown') = 'Seattle' -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/StringTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/StringTranslationsNpgsqlTest.cs deleted file mode 100644 index 2ebc6fb97b..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/StringTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,1571 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; - -namespace Microsoft.EntityFrameworkCore.Query.Translations; - -public class StringTranslationsNpgsqlTest : StringTranslationsRelationalTestBase -{ - public StringTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Equals - - public override async Task Equals() - { - await base.Equals(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'Seattle' -"""); - } - - public override async Task Equals_with_OrdinalIgnoreCase() - { - await base.Equals_with_OrdinalIgnoreCase(); - - AssertSql(); - } - - public override async Task Equals_with_Ordinal() - { - await base.Equals_with_Ordinal(); - - AssertSql(); - } - - public override async Task Static_Equals() - { - await base.Static_Equals(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'Seattle' -"""); - } - - public override async Task Static_Equals_with_OrdinalIgnoreCase() - { - await base.Static_Equals_with_OrdinalIgnoreCase(); - - AssertSql(); - } - - public override async Task Static_Equals_with_Ordinal() - { - await base.Static_Equals_with_Ordinal(); - - AssertSql(); - } - - #endregion Equals - - #region Miscellaneous - - public override async Task Length() - { - await base.Length(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int = 7 -"""); - } - - public override async Task ToUpper() - { - await base.ToUpper(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE upper(b."String") = 'SEATTLE' -""", - // - """ -SELECT upper(b."String") -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task ToLower() - { - await base.ToLower(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE lower(b."String") = 'seattle' -""", - // - """ -SELECT lower(b."String") -FROM "BasicTypesEntities" AS b -"""); - } - - #endregion Miscellaneous - - #region IndexOf - - public override async Task IndexOf() - { - await base.IndexOf(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos(b."String", 'eattl') - 1 <> -1 -"""); - } - - // TODO: #3547 - public override Task IndexOf_Char() - => Assert.ThrowsAsync(() => base.IndexOf_Char()); - - public override async Task IndexOf_with_empty_string() - { - await base.IndexOf_with_empty_string(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos(b."String", '') - 1 = 0 -"""); - } - - public override async Task IndexOf_with_one_parameter_arg() - { - await base.IndexOf_with_one_parameter_arg(); - - AssertSql( - """ -@pattern='eattl' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos(b."String", @pattern) - 1 = 1 -"""); - } - - public override async Task IndexOf_with_one_parameter_arg_char() - { - await base.IndexOf_with_one_parameter_arg_char(); - - AssertSql( - """ -@pattern='e' (DbType = String) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos(b."String", @pattern) - 1 = 1 -"""); - } - - // PostgreSQL does not have strpos with starting position - public override Task IndexOf_with_constant_starting_position() - => AssertTranslationFailed(() => base.IndexOf_with_constant_starting_position()); - - // PostgreSQL does not have strpos with starting position - public override Task IndexOf_with_constant_starting_position_char() - => AssertTranslationFailed(() => base.IndexOf_with_constant_starting_position_char()); - - // PostgreSQL does not have strpos with starting position - public override Task IndexOf_with_parameter_starting_position() - => AssertTranslationFailed(() => base.IndexOf_with_parameter_starting_position()); - - // PostgreSQL does not have strpos with starting position - public override Task IndexOf_with_parameter_starting_position_char() - => AssertTranslationFailed(() => base.IndexOf_with_parameter_starting_position_char()); - - public override async Task IndexOf_after_ToString() - { - await base.IndexOf_after_ToString(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos(b."Int"::text, '55') - 1 = 1 -"""); - } - - public override async Task IndexOf_over_ToString() - { - await base.IndexOf_over_ToString(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos('12559', b."Int"::text) - 1 = 1 -"""); - } - - #endregion IndexOf - - #region Replace - - public override async Task Replace() - { - await base.Replace(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE replace(b."String", 'Sea', 'Rea') = 'Reattle' -"""); - } - - // TODO: #3547 - public override Task Replace_Char() - => AssertTranslationFailed(() => base.Replace_Char()); - - public override async Task Replace_with_empty_string() - { - await base.Replace_with_empty_string(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <> '' AND replace(b."String", b."String", '') = '' -"""); - } - - public override async Task Replace_using_property_arguments() - { - await base.Replace_using_property_arguments(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <> '' AND replace(b."String", b."String", b."Int"::text) = b."Int"::text -"""); - } - - #endregion Replace - - #region Substring - - public override async Task Substring() - { - await base.Substring(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int >= 3 AND substring(b."String", 2, 2) = 'ea' -"""); - } - - public override async Task Substring_with_one_arg_with_zero_startIndex() - { - await base.Substring_with_one_arg_with_zero_startIndex(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE substring(b."String", 1) = 'Seattle' -"""); - } - - public override async Task Substring_with_one_arg_with_constant() - { - await base.Substring_with_one_arg_with_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int >= 1 AND substring(b."String", 2) = 'eattle' -"""); - } - - public override async Task Substring_with_one_arg_with_parameter() - { - await base.Substring_with_one_arg_with_parameter(); - - AssertSql( - """ -@start='2' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int >= 2 AND substring(b."String", @start + 1) = 'attle' -"""); - } - - public override async Task Substring_with_two_args_with_zero_startIndex() - { - await base.Substring_with_two_args_with_zero_startIndex(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int >= 3 AND substring(b."String", 1, 3) = 'Sea' -"""); - } - - public override async Task Substring_with_two_args_with_zero_length() - { - await base.Substring_with_two_args_with_zero_length(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int >= 2 AND substring(b."String", 3, 0) = '' -"""); - } - - public override async Task Substring_with_two_args_with_parameter() - { - await base.Substring_with_two_args_with_parameter(); - - AssertSql( - """ -@start='2' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE length(b."String")::int >= 5 AND substring(b."String", @start + 1, 3) = 'att' -"""); - } - - public override async Task Substring_with_two_args_with_IndexOf() - { - await base.Substring_with_two_args_with_IndexOf(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE '%a%' AND substring(b."String", (strpos(b."String", 'a') - 1) + 1, 3) = 'att' -"""); - } - - #endregion Substring - - #region IsNullOrEmpty/Whitespace - - public override async Task IsNullOrEmpty() - { - await base.IsNullOrEmpty(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."String" IS NULL OR n."String" = '' -""", - // - """ -SELECT n."String" IS NULL OR n."String" = '' -FROM "NullableBasicTypesEntities" AS n -"""); - } - - public override async Task IsNullOrEmpty_negated() - { - await base.IsNullOrEmpty_negated(); - - AssertSql( - """ -SELECT n."Id", n."Bool", n."Byte", n."ByteArray", n."DateOnly", n."DateTime", n."DateTimeOffset", n."Decimal", n."Double", n."Enum", n."FlagsEnum", n."Float", n."Guid", n."Int", n."Long", n."Short", n."String", n."TimeOnly", n."TimeSpan" -FROM "NullableBasicTypesEntities" AS n -WHERE n."String" IS NOT NULL AND n."String" <> '' -""", - // - """ -SELECT n."String" IS NOT NULL AND n."String" <> '' -FROM "NullableBasicTypesEntities" AS n -"""); - } - - public override async Task IsNullOrWhiteSpace() - { - await base.IsNullOrWhiteSpace(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE btrim(b."String", E' \t\n\r') = '' -"""); - } - - #endregion IsNullOrEmpty/Whitespace - - #region StartsWith - - public override async Task StartsWith_Literal() - { - await base.StartsWith_Literal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE 'Se%' -"""); - } - - // TODO: #3547 - public override Task StartsWith_Literal_Char() - => AssertTranslationFailed(() => base.StartsWith_Literal_Char()); - - public override async Task StartsWith_Parameter() - { - await base.StartsWith_Parameter(); - - AssertSql( - """ -@pattern_startswith='Se%' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE @pattern_startswith -"""); - } - - public override Task StartsWith_Parameter_Char() - => AssertTranslationFailed(() => base.StartsWith_Parameter_Char()); - - public override async Task StartsWith_Column() - { - await base.StartsWith_Column(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE left(b."String", length(b."String")) = b."String" -"""); - } - - public override async Task StartsWith_with_StringComparison_Ordinal() - { - await base.StartsWith_with_StringComparison_Ordinal(); - - AssertSql(); - } - - public override async Task StartsWith_with_StringComparison_OrdinalIgnoreCase() - { - await base.StartsWith_with_StringComparison_OrdinalIgnoreCase(); - - AssertSql(); - } - - public override async Task StartsWith_with_StringComparison_unsupported() - { - await base.StartsWith_with_StringComparison_unsupported(); - - AssertSql(); - } - - #endregion StartsWith - - #region EndsWith - - public override async Task EndsWith_Literal() - { - await base.EndsWith_Literal(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE '%le' -"""); - } - - // TODO: #3547 - public override Task EndsWith_Literal_Char() - => AssertTranslationFailed(() => base.EndsWith_Literal_Char()); - - public override async Task EndsWith_Parameter() - { - await base.EndsWith_Parameter(); - - AssertSql( - """ -@pattern_endswith='%le' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE @pattern_endswith -"""); - } - - // TODO: #3547 - public override Task EndsWith_Parameter_Char() - => AssertTranslationFailed(() => base.EndsWith_Parameter_Char()); - - public override async Task EndsWith_Column() - { - // SQL Server trims trailing whitespace for length calculations, making our EndsWith() column translation not work reliably in that - // case - await AssertQuery( - ss => ss.Set().Where(b => b.String == "Seattle" && b.String.EndsWith(b.String))); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'Seattle' AND right(b."String", length(b."String")) = b."String" -"""); - } - - public override async Task EndsWith_with_StringComparison_Ordinal() - { - await base.EndsWith_with_StringComparison_Ordinal(); - - AssertSql(); - } - - public override async Task EndsWith_with_StringComparison_OrdinalIgnoreCase() - { - await base.EndsWith_with_StringComparison_OrdinalIgnoreCase(); - - AssertSql(); - } - - public override async Task EndsWith_with_StringComparison_unsupported() - { - await base.EndsWith_with_StringComparison_unsupported(); - - AssertSql(); - } - - #endregion EndsWith - - #region Contains - - public override async Task Contains_Literal() - { - await AssertQuery( - ss => ss.Set().Where(c => c.String.Contains("eattl")), // SQL Server is case-insensitive by default - ss => ss.Set().Where(c => c.String.Contains("eattl", StringComparison.OrdinalIgnoreCase))); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE '%eattl%' -"""); - } - - // TODO: #3547 - public override Task Contains_Literal_Char() - => AssertTranslationFailed(() => base.Contains_Literal_Char()); - - public override async Task Contains_Column() - { - await base.Contains_Column(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE strpos(b."String", b."String") > 0 -""", - // - """ -SELECT strpos(b."String", b."String") > 0 -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Contains_negated() - { - await base.Contains_negated(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" NOT LIKE '%eattle%' -""", - // - """ -SELECT b."String" NOT LIKE '%eattle%' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task Contains_with_StringComparison_Ordinal() - { - await base.Contains_with_StringComparison_Ordinal(); - - AssertSql(); - } - - public override async Task Contains_with_StringComparison_OrdinalIgnoreCase() - { - await base.Contains_with_StringComparison_OrdinalIgnoreCase(); - - AssertSql(); - } - - public override async Task Contains_with_StringComparison_unsupported() - { - await base.Contains_with_StringComparison_unsupported(); - - AssertSql(); - } - - public override async Task Contains_constant_with_whitespace() - { - await base.Contains_constant_with_whitespace(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE '% %' -"""); - } - - public override async Task Contains_parameter_with_whitespace() - { - await base.Contains_parameter_with_whitespace(); - - AssertSql( - """ -@pattern_contains='% %' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE @pattern_contains -"""); - } - - #endregion Contains - - #region TrimStart - - public override async Task TrimStart_without_arguments() - { - await base.TrimStart_without_arguments(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE ltrim(b."String", E' \t\n\r') = 'Boston ' -"""); - } - - public override async Task TrimStart_with_char_argument() - { - await base.TrimStart_with_char_argument(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE ltrim(b."String", 'S') = 'eattle' -"""); - } - - public override async Task TrimStart_with_char_array_argument() - { - await base.TrimStart_with_char_array_argument(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE ltrim(b."String", 'Se') = 'attle' -"""); - } - - #endregion TrimStart - - #region TrimEnd - - public override async Task TrimEnd_without_arguments() - { - await base.TrimEnd_without_arguments(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE rtrim(b."String", E' \t\n\r') = ' Boston' -"""); - } - - public override async Task TrimEnd_with_char_argument() - { - await base.TrimEnd_with_char_argument(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE rtrim(b."String", 'e') = 'Seattl' -"""); - } - - public override async Task TrimEnd_with_char_array_argument() - { - await base.TrimEnd_with_char_array_argument(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE rtrim(b."String", 'le') = 'Seatt' -"""); - } - - #endregion TrimEnd - - #region Trim - - public override async Task Trim_without_argument_in_predicate() - { - await base.Trim_without_argument_in_predicate(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE btrim(b."String", E' \t\n\r') = 'Boston' -"""); - } - - public override async Task Trim_with_char_argument_in_predicate() - { - await base.Trim_with_char_argument_in_predicate(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE btrim(b."String", 'S') = 'eattle' -"""); - } - - public override async Task Trim_with_char_array_argument_in_predicate() - { - await base.Trim_with_char_array_argument_in_predicate(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE btrim(b."String", 'Se') = 'attl' -"""); - } - - #endregion Trim - - #region Compare - - public override async Task Compare_simple_zero() - { - await base.Compare_simple_zero(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <> 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -"""); - } - - public override async Task Compare_simple_one() - { - await base.Compare_simple_one(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" < 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= 'Seattle' -"""); - } - - public override async Task Compare_with_parameter() - { - await base.Compare_with_parameter(); - - AssertSql( - """ -@basicTypeEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > @basicTypeEntity_String -""", - // - """ -@basicTypeEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" < @basicTypeEntity_String -""", - // - """ -@basicTypeEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= @basicTypeEntity_String -""", - // - """ -@basicTypeEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= @basicTypeEntity_String -""", - // - """ -@basicTypeEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= @basicTypeEntity_String -""", - // - """ -@basicTypeEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= @basicTypeEntity_String -"""); - } - - public override async Task Compare_simple_more_than_one() - { - await base.Compare_simple_more_than_one(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CASE - WHEN b."String" = 'Seattle' THEN 0 - WHEN b."String" > 'Seattle' THEN 1 - WHEN b."String" < 'Seattle' THEN -1 -END = 42 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CASE - WHEN b."String" = 'Seattle' THEN 0 - WHEN b."String" > 'Seattle' THEN 1 - WHEN b."String" < 'Seattle' THEN -1 -END > 42 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE 42 > CASE - WHEN b."String" = 'Seattle' THEN 0 - WHEN b."String" > 'Seattle' THEN 1 - WHEN b."String" < 'Seattle' THEN -1 -END -"""); - } - - public override async Task Compare_nested() - { - await base.Compare_nested(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'M' || b."String" -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <> substring(b."String", 1, 0) -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > replace('Seattle', 'Sea', b."String") -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'M' || b."String" -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > substring(b."String", 1, 0) -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" < replace('Seattle', 'Sea', b."String") -"""); - } - - public override async Task Compare_multi_predicate() - { - await base.Compare_multi_predicate(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= 'Seattle' AND b."String" < 'Toronto' -"""); - } - - public override async Task CompareTo_simple_zero() - { - await base.CompareTo_simple_zero(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <> 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -"""); - } - - public override async Task CompareTo_simple_one() - { - await base.CompareTo_simple_one(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" < 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= 'Seattle' -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= 'Seattle' -"""); - } - - public override async Task CompareTo_with_parameter() - { - await base.CompareTo_with_parameter(); - - AssertSql( - """ -@basicTypesEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > @basicTypesEntity_String -""", - // - """ -@basicTypesEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" < @basicTypesEntity_String -""", - // - """ -@basicTypesEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= @basicTypesEntity_String -""", - // - """ -@basicTypesEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= @basicTypesEntity_String -""", - // - """ -@basicTypesEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= @basicTypesEntity_String -""", - // - """ -@basicTypesEntity_String='Seattle' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= @basicTypesEntity_String -"""); - } - - public override async Task CompareTo_simple_more_than_one() - { - await base.CompareTo_simple_more_than_one(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CASE - WHEN b."String" = 'Seattle' THEN 0 - WHEN b."String" > 'Seattle' THEN 1 - WHEN b."String" < 'Seattle' THEN -1 -END = 42 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CASE - WHEN b."String" = 'Seattle' THEN 0 - WHEN b."String" > 'Seattle' THEN 1 - WHEN b."String" < 'Seattle' THEN -1 -END > 42 -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE 42 > CASE - WHEN b."String" = 'Seattle' THEN 0 - WHEN b."String" > 'Seattle' THEN 1 - WHEN b."String" < 'Seattle' THEN -1 -END -"""); - } - - public override async Task CompareTo_nested() - { - await base.CompareTo_nested(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" = 'M' || b."String" -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <> substring(b."String", 1, 0) -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > replace('Seattle', 'Sea', b."String") -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" <= 'M' || b."String" -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" > substring(b."String", 1, 0) -""", - // - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" < replace('Seattle', 'Sea', b."String") -"""); - } - - public override async Task Compare_to_multi_predicate() - { - await base.Compare_to_multi_predicate(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" >= 'Seattle' AND b."String" < 'Toronto' -"""); - } - - #endregion Compare - - #region Join - - public override async Task Join_over_non_nullable_column() - { - await base.Join_over_non_nullable_column(); - - AssertSql( - """ -SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", '|'), '') AS "Strings" -FROM "BasicTypesEntities" AS b -GROUP BY b."Int" -"""); - } - - public override async Task Join_over_nullable_column() - { - await base.Join_over_nullable_column(); - - AssertSql( - """ -SELECT n0."Key", COALESCE(string_agg(COALESCE(n0."String", ''), '|'), '') AS "Regions" -FROM ( - SELECT n."String", COALESCE(n."Int", 0) AS "Key" - FROM "NullableBasicTypesEntities" AS n -) AS n0 -GROUP BY n0."Key" -"""); - } - - public override async Task Join_with_predicate() - { - await base.Join_with_predicate(); - - AssertSql( - """ -SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", '|') FILTER (WHERE length(b."String")::int > 6), '') AS "Strings" -FROM "BasicTypesEntities" AS b -GROUP BY b."Int" -"""); - } - - public override async Task Join_with_ordering() - { - await base.Join_with_ordering(); - - AssertSql( - """ -SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", '|' ORDER BY b."Id" DESC NULLS LAST), '') AS "Strings" -FROM "BasicTypesEntities" AS b -GROUP BY b."Int" -"""); - } - - public override async Task Join_non_aggregate() - { - await base.Join_non_aggregate(); - - AssertSql( - """ -@foo='foo' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE concat_ws('|', b."String", @foo, '', 'bar') = 'Seattle|foo||bar' -"""); - } - - #endregion Join - - #region Concatenation - - public override async Task Concat_operator() - { - await base.Concat_operator(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" || 'Boston' = 'SeattleBoston' -"""); - } - - public override async Task Concat_aggregate() - { - await base.Concat_aggregate(); - - AssertSql( - """ -SELECT b."Int" AS "Key", COALESCE(string_agg(b."String", ''), '') AS "BasicTypesEntitys" -FROM "BasicTypesEntities" AS b -GROUP BY b."Int" -"""); - } - - public override async Task Concat_string_int_comparison1() - { - await base.Concat_string_int_comparison1(); - - AssertSql( - """ -@i='10' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" || @i::text = 'Seattle10' -"""); - } - - public override async Task Concat_string_int_comparison2() - { - await base.Concat_string_int_comparison2(); - - AssertSql( - """ -@i='10' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE @i::text || b."String" = '10Seattle' -"""); - } - - public override async Task Concat_string_int_comparison3() - { - await base.Concat_string_int_comparison3(); - - AssertSql( - """ -@p='30' -@j='21' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE @p::text || b."String" || @j::text || 42::text = '30Seattle2142' -"""); - } - - public override async Task Concat_string_int_comparison4() - { - await base.Concat_string_int_comparison4(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::text || b."String" = '8Seattle' -"""); - } - - public override async Task Concat_string_string_comparison() - { - await base.Concat_string_string_comparison(); - - AssertSql( - """ -@i='A' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE @i || b."String" = 'ASeattle' -"""); - } - - public override async Task Concat_method_comparison() - { - await base.Concat_method_comparison(); - - AssertSql( - """ -@i='A' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE @i || b."String" = 'ASeattle' -"""); - } - - public override async Task Concat_method_comparison_2() - { - await base.Concat_method_comparison_2(); - - AssertSql( - """ -@i='A' -@j='B' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE @i || @j || b."String" = 'ABSeattle' -"""); - } - - public override async Task Concat_method_comparison_3() - { - await base.Concat_method_comparison_3(); - - AssertSql( - """ -@i='A' -@j='B' -@k='C' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE @i || @j || @k || b."String" = 'ABCSeattle' -"""); - } - - #endregion Concatenation - - #region LINQ Operators - - public override async Task FirstOrDefault() - { - await base.FirstOrDefault(); - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE substr(b."String", 1, 1) = 'S' -"""); - } - - public override async Task LastOrDefault() - { - await base.LastOrDefault(); - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE substr(b."String", length(b."String"), 1) = 'e' -"""); - } - - #endregion LINQ Operators - - #region Like - - public override async Task Where_Like_and_comparison() - { - await base.Where_Like_and_comparison(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE 'S%' AND b."Int" = 8 -"""); - } - - public override async Task Where_Like_or_comparison() - { - await base.Where_Like_or_comparison(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" LIKE 'S%' OR b."Int" = 2147483647 -"""); - } - - public override async Task Like_with_non_string_column_using_ToString() - { - await base.Like_with_non_string_column_using_ToString(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::text LIKE '%5%' -"""); - } - - public override async Task Like_with_non_string_column_using_double_cast() - { - await base.Like_with_non_string_column_using_double_cast(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."Int"::text LIKE '%5%' -"""); - } - - #endregion Like - - #region Regex - - public override async Task Regex_IsMatch() - { - await base.Regex_IsMatch(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."String" ~ '(?p)^S' -"""); - } - - public override async Task Regex_IsMatch_constant_input() - { - await base.Regex_IsMatch_constant_input(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE 'Seattle' ~ ('(?p)' || b."String") -"""); - } - - #endregion Regex - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateOnlyTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateOnlyTranslationsNpgsqlTest.cs deleted file mode 100644 index c89e1ec6e4..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateOnlyTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,238 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; - -public class DateOnlyTranslationsNpgsqlTest : DateOnlyTranslationsTestBase -{ - public DateOnlyTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Year() - { - await base.Year(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('year', b."DateOnly")::int = 1990 -"""); - } - - public override async Task Month() - { - await base.Month(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('month', b."DateOnly")::int = 11 -"""); - } - - public override async Task Day() - { - await base.Day(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('day', b."DateOnly")::int = 10 -"""); - } - - public override async Task DayOfYear() - { - await base.DayOfYear(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('doy', b."DateOnly")::int = 314 -"""); - } - - public override async Task DayOfWeek() - { - await base.DayOfWeek(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(date_part('dow', b."DateOnly"))::int = 6 -"""); - } - - public override async Task DayNumber() - { - await base.DayNumber(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateOnly" - DATE '0001-01-01' = 726780 -"""); - } - - public override async Task AddYears() - { - await base.AddYears(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateOnly" + INTERVAL '3 years' AS date) = DATE '1993-11-10' -"""); - } - - public override async Task AddMonths() - { - await base.AddMonths(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateOnly" + INTERVAL '3 months' AS date) = DATE '1991-02-10' -"""); - } - - public override async Task AddDays() - { - await base.AddDays(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateOnly" + 3 = DATE '1990-11-13' -"""); - } - - public override async Task DayNumber_subtraction() - { - await base.DayNumber_subtraction(); - - AssertSql( - """ -@DayNumber='726775' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE (b."DateOnly" - DATE '0001-01-01') - @DayNumber = 5 -"""); - } - - public override async Task FromDateTime() - { - await base.FromDateTime(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS date) = DATE '1998-05-04' -"""); - } - - public override async Task FromDateTime_compared_to_property() - { - await base.FromDateTime_compared_to_property(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS date) = b."DateOnly" -"""); - } - - public override async Task FromDateTime_compared_to_constant_and_parameter() - { - await base.FromDateTime_compared_to_constant_and_parameter(); - - AssertSql( - """ -@dateOnly='10/11/0002' (DbType = Date) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS date) IN (@dateOnly, DATE '1998-05-04') -"""); - } - - public override async Task ToDateTime_property_with_constant_TimeOnly() - { - await base.ToDateTime_property_with_constant_TimeOnly(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateOnly" + TIME '21:05:19.9405' = TIMESTAMP '2020-01-01T21:05:19.9405' -"""); - } - - public override async Task ToDateTime_property_with_property_TimeOnly() - { - await base.ToDateTime_property_with_property_TimeOnly(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateOnly" + b."TimeOnly" = TIMESTAMP '2020-01-01T15:30:10' -"""); - } - - public override async Task ToDateTime_constant_DateTime_with_property_TimeOnly() - { - await base.ToDateTime_constant_DateTime_with_property_TimeOnly(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE DATE '1990-11-10' + b."TimeOnly" = TIMESTAMP '1990-11-10T15:30:10' -"""); - } - - public override async Task ToDateTime_with_complex_DateTime() - { - await base.ToDateTime_with_complex_DateTime(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateOnly" + INTERVAL '1 years' AS date) + b."TimeOnly" = TIMESTAMP '2021-01-01T15:30:10' -"""); - } - - public override async Task ToDateTime_with_complex_TimeOnly() - { - await base.ToDateTime_with_complex_TimeOnly(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateOnly" + b."TimeOnly" + INTERVAL '1 hours' = TIMESTAMP '2020-01-01T16:30:10' -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeOffsetTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeOffsetTranslationsNpgsqlTest.cs deleted file mode 100644 index 48a27eeb58..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeOffsetTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,243 +0,0 @@ -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; - -public class DateTimeOffsetTranslationsNpgsqlTest : DateTimeOffsetTranslationsTestBase -{ - public DateTimeOffsetTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - // Not supported by design (DateTimeOffset with non-zero offset) - public override Task Now() - => Assert.ThrowsAsync(() => base.Now()); - - public override async Task UtcNow() - { - await base.UtcNow(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTimeOffset" <> now() -"""); - } - - // The test compares with new DateTimeOffset().Date, which Npgsql sends as -infinity, causing a discrepancy with the client behavior - // which uses 1/1/1:0:0:0 - public override Task Date() - => Assert.ThrowsAsync(() => base.Date()); - - public override async Task Year() - { - await base.Year(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('year', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 1998 -"""); - } - - public override async Task Month() - { - await base.Month(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('month', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 5 -"""); - } - - public override async Task DayOfYear() - { - await base.DayOfYear(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('doy', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 124 -"""); - } - - public override async Task Day() - { - await base.Day(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('day', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 4 -"""); - } - - public override async Task Hour() - { - await base.Hour(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('hour', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 15 -"""); - } - - public override async Task Minute() - { - await base.Minute(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('minute', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 30 -"""); - } - - public override async Task Second() - { - await base.Second(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('second', b."DateTimeOffset" AT TIME ZONE 'UTC')::int = 10 -"""); - } - - // SQL translation not implemented, too annoying - public override Task Millisecond() - => AssertTranslationFailed(() => base.Millisecond()); - - // TODO: #3406 - public override Task Microsecond() - => AssertTranslationFailed(() => base.Microsecond()); - - // TODO: #3406 - public override Task Nanosecond() - => AssertTranslationFailed(() => base.Nanosecond()); - - public override async Task TimeOfDay() - { - await base.TimeOfDay(); - - AssertSql( - """ -SELECT CAST(b."DateTimeOffset" AT TIME ZONE 'UTC' AS time) -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddYears() - { - await base.AddYears(); - - AssertSql( - """ -SELECT b."DateTimeOffset" + INTERVAL '1 years' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddMonths() - { - await base.AddMonths(); - - AssertSql( - """ -SELECT b."DateTimeOffset" + INTERVAL '1 months' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddDays() - { - await base.AddDays(); - - AssertSql( - """ -SELECT b."DateTimeOffset" + INTERVAL '1 days' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddHours() - { - await base.AddHours(); - - AssertSql( - """ -SELECT b."DateTimeOffset" + INTERVAL '1 hours' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddMinutes() - { - await base.AddMinutes(); - - AssertSql( - """ -SELECT b."DateTimeOffset" + INTERVAL '1 mins' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddSeconds() - { - await base.AddSeconds(); - - AssertSql( - """ -SELECT b."DateTimeOffset" + INTERVAL '1 secs' -FROM "BasicTypesEntities" AS b -"""); - } - - public override async Task AddMilliseconds() - { - await base.AddMilliseconds(); - - AssertSql( - """ -SELECT b."DateTimeOffset" -FROM "BasicTypesEntities" AS b -"""); - } - - public override Task ToUnixTimeMilliseconds() - => AssertTranslationFailed(() => base.ToUnixTimeMilliseconds()); - - public override Task ToUnixTimeSecond() - => AssertTranslationFailed(() => base.ToUnixTimeSecond()); - - public override async Task Milliseconds_parameter_and_constant() - { - await base.Milliseconds_parameter_and_constant(); - - AssertSql( - """ -SELECT count(*)::int -FROM "BasicTypesEntities" AS b -WHERE b."DateTimeOffset" = TIMESTAMPTZ '1902-01-02T10:00:00.123456+01:30' -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsNpgsqlTest.cs deleted file mode 100644 index 0de15d0a4c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/DateTimeTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; - -namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; - -/// -/// Note that is mapped to PG timestamp with time zone, as is the provider default; -/// this causes issues with various tests. See also , which -/// explicitly maps to timestamp without time zone. -/// -public class DateTimeTranslationsNpgsqlTest : DateTimeTranslationsTestBase -{ - public DateTimeTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Now() - { - await base.Now(); - - AssertSql( - """ -@myDatetime='2015-04-10T00:00:00.0000000' - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE now()::timestamp <> @myDatetime -"""); - } - - public override async Task UtcNow() - { - // Overriding to set Kind=Utc for timestamptz - var myDatetime = DateTime.SpecifyKind(new DateTime(2015, 4, 10), DateTimeKind.Utc); - - await AssertQuery( - ss => ss.Set().Where(c => DateTime.UtcNow != myDatetime)); - - AssertSql( - """ -@myDatetime='2015-04-10T00:00:00.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE now() <> @myDatetime -"""); - } - - // DateTime.Today returns a Local DateTime, which can't be compared with timestamptz - // (see TemporalTranslationsNpgsqlTimestampWithoutTimeZoneTest for a working version of this test) - public override Task Today() - => Assert.ThrowsAsync(() => base.Today()); - - public override async Task Date() - { - // Overriding to set Kind=Utc for timestamptz - var myDatetime = DateTime.SpecifyKind(new DateTime(1998, 5, 4), DateTimeKind.Utc); - - await AssertQuery( - ss => ss.Set().Where(o => o.DateTime.Date == myDatetime)); - - AssertSql( - """ -@myDatetime='1998-05-04T00:00:00.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_trunc('day', b."DateTime", 'UTC') = @myDatetime -"""); - } - - public override async Task AddYear() - { - await base.AddYear(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('year', (b."DateTime" + INTERVAL '1 years') AT TIME ZONE 'UTC')::int = 1999 -"""); - } - - public override async Task Year() - { - await base.Year(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('year', b."DateTime" AT TIME ZONE 'UTC')::int = 1998 -"""); - } - - public override async Task Month() - { - await base.Month(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('month', b."DateTime" AT TIME ZONE 'UTC')::int = 5 -"""); - } - - public override async Task DayOfYear() - { - await base.DayOfYear(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('doy', b."DateTime" AT TIME ZONE 'UTC')::int = 124 -"""); - } - - public override async Task Day() - { - await base.Day(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('day', b."DateTime" AT TIME ZONE 'UTC')::int = 4 -"""); - } - - public override async Task Hour() - { - await base.Hour(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('hour', b."DateTime" AT TIME ZONE 'UTC')::int = 15 -"""); - } - - public override async Task Minute() - { - await base.Minute(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('minute', b."DateTime" AT TIME ZONE 'UTC')::int = 30 -"""); - } - - public override async Task Second() - { - await base.Second(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('second', b."DateTime" AT TIME ZONE 'UTC')::int = 10 -"""); - } - - // SQL translation not implemented, too annoying - public override Task Millisecond() - => AssertTranslationFailed(() => base.Millisecond()); - - public override async Task TimeOfDay() - { - await base.TimeOfDay(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time) = TIME '00:00:00' -"""); - } - - public override async Task subtract_and_TotalDays() - { - // Overriding to set Kind=Utc for timestamptz - var date = DateTime.SpecifyKind(new DateTime(1997, 1, 1), DateTimeKind.Utc); - - await AssertQuery( - ss => ss.Set().Where(o => (o.DateTime - date).TotalDays > 365)); - - AssertSql( - """ -@date='1997-01-01T00:00:00.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('epoch', b."DateTime" - @date) / 86400.0 > 365.0 -"""); - } - - // DateTime.Parse() returns either a Local or Unspecified DateTime, which can't be compared with timestamptz - // (see TemporalTranslationsNpgsqlTimestampWithoutTimeZoneTest for a working version of this test) - public override Task Parse_with_constant() - => Assert.ThrowsAsync(() => base.Parse_with_constant()); - - // DateTime.Parse() returns either a Local or Unspecified DateTime, which can't be compared with timestamptz - // (see TemporalTranslationsNpgsqlTimestampWithoutTimeZoneTest for a working version of this test) - public override Task Parse_with_parameter() - => Assert.ThrowsAsync(() => base.Parse_with_parameter()); - - public override async Task New_with_constant() - { - // Overriding to set Kind=Utc for timestamptz - await AssertQuery( - ss => ss.Set().Where(o => o.DateTime == new DateTime(1998, 5, 4, 15, 30, 10, DateTimeKind.Utc))); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" = TIMESTAMPTZ '1998-05-04T15:30:10Z' -"""); - } - - public override async Task New_with_parameters() - { - // Overriding to set Kind=Utc for timestamptz - var year = 1998; - var month = 5; - var date = 4; - var hour = 15; - - await AssertQuery( - ss => ss.Set().Where(o => o.DateTime == new DateTime(year, month, date, hour, 30, 10, DateTimeKind.Utc))); - - AssertSql( - """ -@p='1998-05-04T15:30:10.0000000Z' (DbType = DateTime) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."DateTime" = @p -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsNpgsqlTest.cs deleted file mode 100644 index 5f03e4f5f0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/TimeOnlyTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.BasicTypesModel; - -namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; - -public class TimeOnlyTranslationsNpgsqlTest : TimeOnlyTranslationsTestBase -{ - public TimeOnlyTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Hour() - { - await base.Hour(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('hour', b."TimeOnly")::int = 15 -"""); - } - - public override async Task Minute() - { - await base.Minute(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('minute', b."TimeOnly")::int = 30 -"""); - } - - public override async Task Second() - { - await base.Second(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE date_part('second', b."TimeOnly")::int = 10 -"""); - } - - // Translation not yet implemented - public override Task Millisecond() - => AssertTranslationFailed(() => base.Millisecond()); - - // Translation not yet implemented - public override Task Microsecond() - => AssertTranslationFailed(() => base.Millisecond()); - - // Probably not relevant for PostgreSQL, which supports microsecond precision only - public override Task Nanosecond() - => AssertTranslationFailed(() => base.Millisecond()); - - public override async Task AddHours() - { - await base.AddHours(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeOnly" + INTERVAL '3 hours' = TIME '18:30:10' -"""); - } - - public override async Task AddMinutes() - { - await base.AddMinutes(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeOnly" + INTERVAL '3 mins' = TIME '15:33:10' -"""); - } - - public override async Task Add_TimeSpan() - { - await base.Add_TimeSpan(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeOnly" + INTERVAL '03:00:00' = TIME '18:30:10' -"""); - } - - public override async Task IsBetween() - { - await base.IsBetween(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeOnly" >= TIME '14:00:00' AND b."TimeOnly" < TIME '16:00:00' -"""); - } - - public override async Task Subtract() - { - await base.Subtract(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeOnly" - TIME '03:00:00' = INTERVAL '12:30:10' -"""); - } - - public override async Task FromDateTime_compared_to_property() - { - await base.FromDateTime_compared_to_property(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time without time zone) = b."TimeOnly" -"""); - } - - public override async Task FromDateTime_compared_to_parameter() - { - await base.FromDateTime_compared_to_parameter(); - - AssertSql( - """ -@time='15:30' (DbType = Time) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time without time zone) = @time -"""); - } - - public override async Task FromDateTime_compared_to_constant() - { - await base.FromDateTime_compared_to_constant(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE CAST(b."DateTime" AT TIME ZONE 'UTC' AS time without time zone) = TIME '15:30:10' -"""); - } - - public override async Task FromTimeSpan_compared_to_property() - { - await base.FromTimeSpan_compared_to_property(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan"::time without time zone < b."TimeOnly" -"""); - } - - public override async Task FromTimeSpan_compared_to_parameter() - { - await base.FromTimeSpan_compared_to_parameter(); - - AssertSql( - """ -@time='01:02' (DbType = Time) - -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE b."TimeSpan"::time without time zone = @time -"""); - } - - public override async Task Order_by_FromTimeSpan() - { - // TODO: Base implementation is non-deterministic, remove this override once that's fixed on the EF side. - await AssertQuery( - ss => ss.Set().OrderBy(x => TimeOnly.FromTimeSpan(x.TimeSpan)).ThenBy(x => x.Id), - assertOrder: true); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -ORDER BY b."TimeSpan"::time without time zone NULLS FIRST, b."Id" NULLS FIRST -"""); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsNpgsqlTest.cs deleted file mode 100644 index c32ecd9369..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/Translations/Temporal/TimeSpanTranslationsNpgsqlTest.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query.Translations.Temporal; - -public class TimeSpanTranslationsNpgsqlTest : TimeSpanTranslationsTestBase -{ - public TimeSpanTranslationsNpgsqlTest(BasicTypesQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - public override async Task Hours() - { - await base.Hours(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(date_part('hour', b."TimeSpan"))::int = 3 -"""); - } - - public override async Task Minutes() - { - await base.Minutes(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(date_part('minute', b."TimeSpan"))::int = 4 -"""); - } - - public override async Task Seconds() - { - await base.Seconds(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(date_part('second', b."TimeSpan"))::int = 5 -"""); - } - - public override async Task Milliseconds() - { - await base.Milliseconds(); - - AssertSql( - """ -SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan" -FROM "BasicTypesEntities" AS b -WHERE floor(date_part('millisecond', b."TimeSpan"))::int % 1000 = 678 -"""); - } - - public override Task Microseconds() - => AssertTranslationFailed(() => base.Microseconds()); - - public override Task Nanoseconds() - => AssertTranslationFailed(() => base.Nanoseconds()); - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs b/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs deleted file mode 100644 index 334ec25b9c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/UdfDbFunctionNpgsqlTests.cs +++ /dev/null @@ -1,785 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class UdfDbFunctionNpgsqlTests : UdfDbFunctionTestBase -{ - // ReSharper disable once UnusedParameter.Local - public UdfDbFunctionNpgsqlTests(UdfNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - #region Static - - [Fact] - public override void Scalar_Function_Extension_Method_Static() - { - base.Scalar_Function_Extension_Method_Static(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE "IsDate"(c."FirstName") = FALSE -"""); - } - - [Fact] - public override void Scalar_Function_With_Translator_Translates_Static() - { - base.Scalar_Function_With_Translator_Translates_Static(); - - AssertSql( - """ -@customerId='3' - -SELECT length(c."LastName") -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_ClientEval_Method_As_Translateable_Method_Parameter_Static() - => base.Scalar_Function_ClientEval_Method_As_Translateable_Method_Parameter_Static(); - - [Fact] - public override void Scalar_Function_Constant_Parameter_Static() - { - base.Scalar_Function_Constant_Parameter_Static(); - - AssertSql( - """ -@customerId='1' - -SELECT "CustomerOrderCount"(@customerId) -FROM "Customers" AS c -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Correlated_Static() - { - base.Scalar_Function_Anonymous_Type_Select_Correlated_Static(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 1 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Not_Correlated_Static() - { - base.Scalar_Function_Anonymous_Type_Select_Not_Correlated_Static(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(1) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 1 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Parameter_Static() - { - base.Scalar_Function_Anonymous_Type_Select_Parameter_Static(); - - AssertSql( - """ -@customerId='1' - -SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Nested_Static() - { - base.Scalar_Function_Anonymous_Type_Select_Nested_Static(); - - AssertSql( - """ -@starCount='3' -@customerId='3' - -SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Where_Correlated_Static() - { - base.Scalar_Function_Where_Correlated_Static(); - - AssertSql( - """ -SELECT lower(c."Id"::text) -FROM "Customers" AS c -WHERE "IsTopCustomer"(c."Id") -"""); - } - - [Fact(Skip = "https://github.com/dotnet/efcore/issues/25980")] - public override void Scalar_Function_Where_Not_Correlated_Static() - { - base.Scalar_Function_Where_Not_Correlated_Static(); - - AssertSql( - """ -@__startDate_0='2000-04-01T00:00:00.0000000' (Nullable = true) (DbType = DateTime) - -SELECT c."Id" -FROM "Customers" AS c -WHERE "GetCustomerWithMostOrdersAfterDate"(@__startDate_0) = c."Id" -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Where_Parameter_Static() - { - base.Scalar_Function_Where_Parameter_Static(); - - AssertSql( - """ -@period='0' - -SELECT c."Id" -FROM "Customers" AS c -WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(@period)) -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Where_Nested_Static() - { - base.Scalar_Function_Where_Nested_Static(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(0)) -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Correlated_Static() - { - base.Scalar_Function_Let_Correlated_Static(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 2 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Not_Correlated_Static() - { - base.Scalar_Function_Let_Not_Correlated_Static(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(2) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 2 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Not_Parameter_Static() - { - base.Scalar_Function_Let_Not_Parameter_Static(); - - AssertSql( - """ -@customerId='2' - -SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Nested_Static() - { - base.Scalar_Function_Let_Nested_Static(); - - AssertSql( - """ -@starCount='3' -@customerId='1' - -SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Nested_Function_Unwind_Client_Eval_Select_Static() - { - base.Scalar_Nested_Function_Unwind_Client_Eval_Select_Static(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -ORDER BY c."Id" NULLS FIRST -"""); - } - - [Fact] - public override void Scalar_Nested_Function_BCL_UDF_Static() - { - base.Scalar_Nested_Function_BCL_UDF_Static(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -WHERE 3 = abs("CustomerOrderCount"(c."Id")) -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Nested_Function_UDF_BCL_Static() - { - base.Scalar_Nested_Function_UDF_BCL_Static(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -WHERE 3 = "CustomerOrderCount"(abs(c."Id")) -LIMIT 2 -"""); - } - - [Fact] - public override void Nullable_navigation_property_access_preserves_schema_for_sql_function() - { - base.Nullable_navigation_property_access_preserves_schema_for_sql_function(); - - AssertSql( - """ -SELECT dbo."IdentityString"(c."FirstName") -FROM "Orders" AS o -INNER JOIN "Customers" AS c ON o."CustomerId" = c."Id" -ORDER BY o."Id" NULLS FIRST -LIMIT 1 -"""); - } - - #endregion - - #region Instance - - [Fact] - public override void Scalar_Function_Non_Static() - { - base.Scalar_Function_Non_Static(); - - AssertSql( - """ -SELECT "StarValue"(4, c."Id") AS "Id", "DollarValue"(2, c."LastName") AS "LastName" -FROM "Customers" AS c -WHERE c."Id" = 1 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Extension_Method_Instance() - { - base.Scalar_Function_Extension_Method_Instance(); - - AssertSql( - """ -SELECT count(*)::int -FROM "Customers" AS c -WHERE "IsDate"(c."FirstName") = FALSE -"""); - } - - [Fact] - public override void Scalar_Function_With_Translator_Translates_Instance() - { - base.Scalar_Function_With_Translator_Translates_Instance(); - - AssertSql( - """ -@customerId='3' - -SELECT length(c."LastName") -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Constant_Parameter_Instance() - { - base.Scalar_Function_Constant_Parameter_Instance(); - - AssertSql( - """ -@customerId='1' - -SELECT "CustomerOrderCount"(@customerId) -FROM "Customers" AS c -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Correlated_Instance() - { - base.Scalar_Function_Anonymous_Type_Select_Correlated_Instance(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 1 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Not_Correlated_Instance() - { - base.Scalar_Function_Anonymous_Type_Select_Not_Correlated_Instance(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(1) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 1 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Parameter_Instance() - { - base.Scalar_Function_Anonymous_Type_Select_Parameter_Instance(); - - AssertSql( - """ -@customerId='1' - -SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Anonymous_Type_Select_Nested_Instance() - { - base.Scalar_Function_Anonymous_Type_Select_Nested_Instance(); - - AssertSql( - """ -@starCount='3' -@customerId='3' - -SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Where_Correlated_Instance() - { - base.Scalar_Function_Where_Correlated_Instance(); - - AssertSql( - """ -SELECT lower(c."Id"::text) -FROM "Customers" AS c -WHERE "IsTopCustomer"(c."Id") -"""); - } - - [Fact(Skip = "https://github.com/dotnet/efcore/issues/25980")] - public override void Scalar_Function_Where_Not_Correlated_Instance() - { - base.Scalar_Function_Where_Not_Correlated_Instance(); - - AssertSql( - """ -@__startDate_1='2000-04-01T00:00:00.0000000' (Nullable = true) (DbType = DateTime) - -SELECT c."Id" -FROM "Customers" AS c -WHERE "GetCustomerWithMostOrdersAfterDate"(@__startDate_1) = c."Id" -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Where_Parameter_Instance() - { - base.Scalar_Function_Where_Parameter_Instance(); - - AssertSql( - """ -@period='0' - -SELECT c."Id" -FROM "Customers" AS c -WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(@period)) -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Where_Nested_Instance() - { - base.Scalar_Function_Where_Nested_Instance(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -WHERE c."Id" = "GetCustomerWithMostOrdersAfterDate"("GetReportingPeriodStartDate"(0)) -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Correlated_Instance() - { - base.Scalar_Function_Let_Correlated_Instance(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(c."Id") AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 2 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Not_Correlated_Instance() - { - base.Scalar_Function_Let_Not_Correlated_Instance(); - - AssertSql( - """ -SELECT c."LastName", "CustomerOrderCount"(2) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = 2 -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Not_Parameter_Instance() - { - base.Scalar_Function_Let_Not_Parameter_Instance(); - - AssertSql( - """ -@customerId='2' - -SELECT c."LastName", "CustomerOrderCount"(@customerId) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Function_Let_Nested_Instance() - { - base.Scalar_Function_Let_Nested_Instance(); - - AssertSql( - """ -@starCount='3' -@customerId='1' - -SELECT c."LastName", "StarValue"(@starCount, "CustomerOrderCount"(@customerId)) AS "OrderCount" -FROM "Customers" AS c -WHERE c."Id" = @customerId -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Nested_Function_Unwind_Client_Eval_Select_Instance() - { - base.Scalar_Nested_Function_Unwind_Client_Eval_Select_Instance(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -ORDER BY c."Id" NULLS FIRST -"""); - } - - [Fact] - public override void Scalar_Nested_Function_BCL_UDF_Instance() - { - base.Scalar_Nested_Function_BCL_UDF_Instance(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -WHERE 3 = abs("CustomerOrderCount"(c."Id")) -LIMIT 2 -"""); - } - - [Fact] - public override void Scalar_Nested_Function_UDF_BCL_Instance() - { - base.Scalar_Nested_Function_UDF_BCL_Instance(); - - AssertSql( - """ -SELECT c."Id" -FROM "Customers" AS c -WHERE 3 = "CustomerOrderCount"(abs(c."Id")) -LIMIT 2 -"""); - } - - #endregion - -#if RELEASE - [ConditionalFact(Skip = "https://github.com/dotnet/efcore/pull/30388")] - public override void Scalar_Function_with_nullable_value_return_type_throws() {} -#endif - - protected class NpgsqlUDFSqlContext(DbContextOptions options) : UDFSqlContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // IsDate is a built-in SQL Server function, that in the base class is mapped as built-in, which means we - // don't get any quotes. We remap it as non-built-in by including a (null) schema. - var isDateMethodInfo = typeof(UDFSqlContext).GetMethod(nameof(IsDateStatic)); - modelBuilder.HasDbFunction(isDateMethodInfo) - .HasTranslation( - args => new SqlFunctionExpression( - schema: null, - "IsDate", - args, - nullable: true, - argumentsPropagateNullability: args.Select(_ => true).ToList(), - isDateMethodInfo.ReturnType, - typeMapping: null)); - - var isDateMethodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(IsDateInstance)); - modelBuilder.HasDbFunction(isDateMethodInfo2) - .HasTranslation( - args => new SqlFunctionExpression( - schema: null, - "IsDate", - args, - nullable: true, - argumentsPropagateNullability: args.Select(_ => true).ToList(), - isDateMethodInfo2.ReturnType, - typeMapping: null)); - - // Base class maps to len(), but in PostgreSQL it's called length() - var methodInfo = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthStatic)); - modelBuilder.HasDbFunction(methodInfo) - .HasTranslation( - args => new SqlFunctionExpression( - "length", - args, - nullable: true, - argumentsPropagateNullability: args.Select(_ => true).ToList(), - methodInfo.ReturnType, - typeMapping: null)); - - var methodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthInstance)); - modelBuilder.HasDbFunction(methodInfo2) - .HasTranslation( - args => new SqlFunctionExpression( - "length", - args, - nullable: true, - argumentsPropagateNullability: args.Select(_ => true).ToList(), - methodInfo2.ReturnType, - typeMapping: null)); - } - } - - public class UdfNpgsqlFixture : UdfFixtureBase - { - protected override string StoreName { get; } = "UDFDbFunctionNpgsqlTests"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override Type ContextType { get; } = typeof(NpgsqlUDFSqlContext); - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // We default to mapping DateTime to 'timestamp with time zone', but the seeding data has Unspecified DateTimes which aren't - // supported. - modelBuilder.Entity().Property(o => o.OrderDate).HasColumnType("timestamp without time zone"); - - // The following should make us send 'timestamp without time zone' for these functions, but it doesn't: - // https://github.com/dotnet/efcore/issues/25980 - var typeMappingSource = context.GetService(); - modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(UDFSqlContext.GetCustomerWithMostOrdersAfterDateStatic))) - .HasParameter("startDate").Metadata.TypeMapping = typeMappingSource.GetMapping("timestamp without time zone"); - modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(UDFSqlContext.GetCustomerWithMostOrdersAfterDateInstance))) - .HasParameter("startDate").Metadata.TypeMapping = typeMappingSource.GetMapping("timestamp without time zone"); - } - - protected override async Task SeedAsync(DbContext context) - { - await base.SeedAsync(context); - - await context.Database.ExecuteSqlRawAsync( - """ -CREATE FUNCTION "CustomerOrderCount" ("customerId" INTEGER) RETURNS INTEGER -AS $$ SELECT COUNT("Id")::INTEGER FROM "Orders" WHERE "CustomerId" = $1 $$ -LANGUAGE SQL; - -CREATE FUNCTION "StarValue" ("starCount" INTEGER, value TEXT) RETURNS TEXT -AS $$ SELECT repeat('*', $1) || $2 $$ -LANGUAGE SQL; - -CREATE FUNCTION "StarValue" ("starCount" INTEGER, value INTEGER) RETURNS TEXT -AS $$ SELECT repeat('*', $1) || $2 $$ -LANGUAGE SQL; - -CREATE FUNCTION "DollarValue" ("starCount" INTEGER, value TEXT) RETURNS TEXT -AS $$ SELECT repeat('$', $1) || $2 $$ -LANGUAGE SQL; - -CREATE FUNCTION "GetReportingPeriodStartDate" (period INTEGER) RETURNS DATE -AS $$ SELECT DATE '1998-01-01' $$ -LANGUAGE SQL; - -CREATE FUNCTION "GetCustomerWithMostOrdersAfterDate" (searchDate TIMESTAMP) RETURNS INTEGER -AS $$ - SELECT "CustomerId" - FROM "Orders" - WHERE "OrderDate" > $1 - GROUP BY "CustomerId" - ORDER BY COUNT("Id") DESC - LIMIT 1 -$$ LANGUAGE SQL; - -CREATE FUNCTION "IsTopCustomer" ("customerId" INTEGER) RETURNS BOOL -AS $$ SELECT $1 = 1 $$ -LANGUAGE SQL; - -CREATE SCHEMA IF NOT EXISTS dbo; - -CREATE FUNCTION dbo."IdentityString" ("customerName" TEXT) RETURNS TEXT -AS $$ SELECT $1 $$ -LANGUAGE SQL; - -CREATE FUNCTION "GetCustomerOrderCountByYear"("customerId" INT) -RETURNS TABLE ("CustomerId" INT, "Count" INT, "Year" INT) -AS $$ - SELECT "CustomerId", COUNT("Id")::INT, EXTRACT(year FROM "OrderDate")::INT - FROM "Orders" - WHERE "CustomerId" = $1 - GROUP BY "CustomerId", EXTRACT(year FROM "OrderDate") - ORDER BY EXTRACT(year FROM "OrderDate") -$$ LANGUAGE SQL; - -CREATE FUNCTION "StringLength"("s" TEXT) RETURNS INT -AS $$ SELECT LENGTH($1) $$ -LANGUAGE SQL; - -CREATE FUNCTION "GetCustomerOrderCountByYearOnlyFrom2000"("customerId" INT, "onlyFrom2000" BOOL) -RETURNS TABLE ("CustomerId" INT, "Count" INT, "Year" INT) -AS $$ - SELECT $1, COUNT("Id")::INT, EXTRACT(year FROM "OrderDate")::INT - FROM "Orders" - WHERE "CustomerId" = 1 AND (NOT $2 OR $2 IS NULL OR ($2 AND EXTRACT(year FROM "OrderDate") = 2000)) - GROUP BY "CustomerId", EXTRACT(year FROM "OrderDate") - ORDER BY EXTRACT(year FROM "OrderDate") -$$ LANGUAGE SQL; - -CREATE FUNCTION "GetTopTwoSellingProducts"() -RETURNS TABLE ("ProductId" INT, "AmountSold" INT) -AS $$ - SELECT "ProductId", SUM("Quantity")::INT AS "totalSold" - FROM "LineItem" - GROUP BY "ProductId" - ORDER BY "totalSold" DESC - LIMIT 2 -$$ LANGUAGE SQL; - -CREATE FUNCTION "GetOrdersWithMultipleProducts"("customerId" INT) -RETURNS TABLE ("OrderId" INT, "CustomerId" INT, "OrderDate" TIMESTAMP) -AS $$ - SELECT o."Id", $1, "OrderDate" - FROM "Orders" AS o - JOIN "LineItem" li ON o."Id" = li."OrderId" - WHERE o."CustomerId" = $1 - GROUP BY o."Id", "OrderDate" - HAVING COUNT("ProductId") > 1 -$$ LANGUAGE SQL; - -CREATE FUNCTION "AddValues" (a INT, b INT) RETURNS INT -AS $$ SELECT $1 + $2 $$ LANGUAGE SQL; - -CREATE FUNCTION "IsDate"(s TEXT) RETURNS BOOLEAN -AS $$ -BEGIN - PERFORM s::DATE; - RETURN TRUE; -EXCEPTION WHEN OTHERS THEN - RETURN FALSE; -END; -$$ LANGUAGE PLPGSQL; -"""); - - await context.SaveChangesAsync(); - } - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs deleted file mode 100644 index 1956bf82e1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Query/WarningsNpgsqlTest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Query; - -public class WarningsNpgsqlTest(QueryNoClientEvalNpgsqlFixture fixture) : WarningsTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs b/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs deleted file mode 100644 index db4f15a9ec..0000000000 --- a/test/EFCore.PG.FunctionalTests/QueryExpressionInterceptionNpgsqlTestBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore; - -public abstract class QueryExpressionInterceptionNpgsqlTestBase( - QueryExpressionInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) - : QueryExpressionInterceptionTestBase(fixture) -{ - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase - { - 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; - } - } - - public class QueryExpressionInterceptionNpgsqlTest(QueryExpressionInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) - : QueryExpressionInterceptionNpgsqlTestBase(fixture), IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override string StoreName - => "QueryExpressionInterception"; - - protected override bool ShouldSubscribeToDiagnosticListener - => false; - } - } - - public class QueryExpressionInterceptionWithDiagnosticsNpgsqlTest( - QueryExpressionInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) - : QueryExpressionInterceptionNpgsqlTestBase(fixture), - IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override string StoreName - => "QueryExpressionInterceptionWithDiagnostics"; - - protected override bool ShouldSubscribeToDiagnosticListener - => true; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs deleted file mode 100644 index 4c3830766a..0000000000 --- a/test/EFCore.PG.FunctionalTests/SaveChangesInterceptionNpgsqlTest.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore; - -public abstract class SaveChangesInterceptionNpgsqlTestBase(SaveChangesInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) - : SaveChangesInterceptionTestBase(fixture) -{ - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase - { - 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; - } - } - - public class SaveChangesInterceptionNpgsqlTest(SaveChangesInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) - : SaveChangesInterceptionNpgsqlTestBase(fixture), IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override string StoreName - => "SaveChangesInterception"; - - protected override bool ShouldSubscribeToDiagnosticListener - => false; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder(base.AddOptions(builder)) - .ExecutionStrategy(d => new NpgsqlExecutionStrategy(d)); - return builder; - } - } - } - - public class SaveChangesInterceptionWithDiagnosticsNpgsqlTest( - SaveChangesInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) - : SaveChangesInterceptionNpgsqlTestBase(fixture), - IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override string StoreName - => "SaveChangesInterceptionWithDiagnostics"; - - protected override bool ShouldSubscribeToDiagnosticListener - => true; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder(base.AddOptions(builder)) - .ExecutionStrategy(d => new NpgsqlExecutionStrategy(d)); - return builder; - } - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Scaffolding/CompiledModelNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Scaffolding/CompiledModelNpgsqlTest.cs deleted file mode 100644 index f2ad5e6ac0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Scaffolding/CompiledModelNpgsqlTest.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index 7cdfe3d885..0000000000 --- a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs +++ /dev/null @@ -1,2229 +0,0 @@ -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Storage.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -// ReSharper disable InconsistentNaming -// ReSharper disable StringLiteralTypo -namespace Microsoft.EntityFrameworkCore.Scaffolding; - -public class NpgsqlDatabaseModelFactoryTest : IClassFixture -{ - // ReSharper disable once MemberCanBePrivate.Global - protected NpgsqlDatabaseModelFixture Fixture { get; } - - public NpgsqlDatabaseModelFactoryTest(NpgsqlDatabaseModelFixture fixture) - { - Fixture = fixture; - Fixture.ListLoggerFactory.Clear(); - } - - #region Sequences - - [Fact] - public void Create_sequences_with_facets() - { - var supportsDataType = TestEnvironment.PostgresVersion >= new Version(10, 0); - - Test( - $""" -CREATE SEQUENCE "DefaultFacetsSequence"; - -CREATE SEQUENCE db2."CustomFacetsSequence" - {(supportsDataType ? "AS int" : null)} - START WITH 1 - INCREMENT BY 2 - MAXVALUE 8 - MINVALUE -3 - CYCLE; -""", - [], - [], - dbModel => - { - var defaultSequence = dbModel.Sequences.First(ds => ds.Name == "DefaultFacetsSequence"); - Assert.Equal("public", defaultSequence.Schema); - Assert.Equal("DefaultFacetsSequence", defaultSequence.Name); - Assert.Equal("bigint", defaultSequence.StoreType); - Assert.Null(defaultSequence.IsCyclic); - Assert.Null(defaultSequence.IncrementBy); - Assert.Null(defaultSequence.StartValue); - Assert.Null(defaultSequence.MinValue); - Assert.Null(defaultSequence.MaxValue); - - var customSequence = dbModel.Sequences.First(ds => ds.Name == "CustomFacetsSequence"); - Assert.Equal("db2", customSequence.Schema); - Assert.Equal("CustomFacetsSequence", customSequence.Name); - Assert.Equal(supportsDataType ? "integer" : "bigint", customSequence.StoreType); - Assert.True(customSequence.IsCyclic); - Assert.Equal(2, customSequence.IncrementBy); - Assert.Equal(1, customSequence.StartValue); - Assert.Equal(-3, customSequence.MinValue); - Assert.Equal(8, customSequence.MaxValue); - }, - """ -DROP SEQUENCE "DefaultFacetsSequence"; -DROP SEQUENCE db2."CustomFacetsSequence" -"""); - } - - [ConditionalFact] - [MinimumPostgresVersion(11, 0)] - public void Sequence_min_max_start_values_are_null_if_default() - => Test( - """ -CREATE SEQUENCE "SmallIntSequence" AS smallint; -CREATE SEQUENCE "IntSequence" AS int; -CREATE SEQUENCE "BigIntSequence" AS bigint; -""", - [], - [], - dbModel => - { - Assert.All( - dbModel.Sequences, - s => - { - Assert.Null(s.StartValue); - Assert.Null(s.MinValue); - Assert.Null(s.MaxValue); - }); - }, - """ -DROP SEQUENCE "SmallIntSequence"; -DROP SEQUENCE "IntSequence"; -DROP SEQUENCE "BigIntSequence"; -"""); - - [Fact] - public void Filter_sequences_based_on_schema() - => Test( - """ -CREATE SEQUENCE "Sequence"; -CREATE SEQUENCE db2."Sequence" -""", - [], - ["db2"], - dbModel => - { - var sequence = Assert.Single(dbModel.Sequences); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("db2", sequence.Schema); - Assert.Equal("Sequence", sequence.Name); - Assert.Equal("bigint", sequence.StoreType); - }, - """ -DROP SEQUENCE "Sequence"; -DROP SEQUENCE db2."Sequence"; -"""); - - #endregion - - #region Model - - [Fact] - public void Set_default_schema() - => Test( - "SELECT 1", - [], - [], - dbModel => - { - Assert.Equal("public", dbModel.DefaultSchema); - }, - null); - - [Fact] - public void Create_tables() - => Test( - """ -CREATE TABLE "Everest" (id int); -CREATE TABLE "Denali" (id int); -""", - [], - [], - dbModel => - { - Assert.Collection( - dbModel.Tables.OrderBy(t => t.Name), - d => - { - Assert.Equal("public", d.Schema); - Assert.Equal("Denali", d.Name); - }, - e => - { - Assert.Equal("public", e.Schema); - Assert.Equal("Everest", e.Name); - }); - }, - """ -DROP TABLE "Everest"; -DROP TABLE "Denali"; -"""); - - #endregion - - #region FilteringSchemaTable - - [Fact] - public void Filter_schemas() - => Test( - """ -CREATE TABLE db2."K2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); -""", - [], - ["db2"], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "Kilimanjaro"; -DROP TABLE db2."K2"; -"""); - - [Fact] - public void Filter_tables() - => Test( - """ -CREATE TABLE "K2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B), FOREIGN KEY (B) REFERENCES "K2" (A)); -""", - ["K2"], - [], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "Kilimanjaro"; -DROP TABLE "K2"; -"""); - - [Fact] - public void Filter_tables_with_qualified_name() - => Test( - """ -CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); -""", - [""" - "K.2" - """], - [], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K.2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "Kilimanjaro"; -DROP TABLE "K.2"; -"""); - - [Fact] - public void Filter_tables_with_schema_qualified_name1() - => Test( - """ -CREATE TABLE public."K2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE db2."K2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); -""", - ["public.K2"], - [], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "Kilimanjaro"; -DROP TABLE "K2"; -DROP TABLE db2."K2"; -"""); - - [Fact] - public void Filter_tables_with_schema_qualified_name2() - => Test( - """ -CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "db.2"."K.2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "db.2"."Kilimanjaro" (Id int, B varchar, UNIQUE (B)); -""", - [""" - "db.2"."K.2" - """], - [], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K.2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "db.2"."Kilimanjaro"; -DROP TABLE "K.2"; -DROP TABLE "db.2"."K.2"; -"""); - - [Fact] - public void Filter_tables_with_schema_qualified_name3() - => Test( - """ -CREATE TABLE "K.2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "db2"."K.2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "Kilimanjaro" (Id int, B varchar, UNIQUE (B)); -""", - [""" - public."K.2" - """], - [], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K.2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "Kilimanjaro"; -DROP TABLE "K.2"; -DROP TABLE db2."K.2"; -"""); - - [Fact] - public void Filter_tables_with_schema_qualified_name4() - => Test( - """ -CREATE TABLE "K2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "db.2"."K2" (Id int, A varchar, UNIQUE (A)); -CREATE TABLE "db.2"."Kilimanjaro" (Id int, B varchar, UNIQUE (B)); -""", - [""" - "db.2".K2 - """], - [], - dbModel => - { - var table = Assert.Single(dbModel.Tables); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("K2", table.Name); - Assert.Equal(2, table.Columns.Count); - Assert.Single(table.UniqueConstraints); - Assert.Empty(table.ForeignKeys); - }, - """ -DROP TABLE "db.2"."Kilimanjaro"; -DROP TABLE "K2"; -DROP TABLE "db.2"."K2"; -"""); - - [Fact] - public void Complex_filtering_validation() - => Test( - """ -CREATE SEQUENCE public."Sequence"; -CREATE SEQUENCE "db2"."Sequence"; - -CREATE TABLE "db.2"."QuotedTableName" ("Id" int PRIMARY KEY); -CREATE TABLE "db.2"."Table.With.Dot" ("Id" int PRIMARY KEY); -CREATE TABLE "db.2"."SimpleTableName" ("Id" int PRIMARY KEY); -CREATE TABLE "db.2"."JustTableName" ("Id" int PRIMARY KEY); - -CREATE TABLE public."QuotedTableName" ("Id" int PRIMARY KEY); -CREATE TABLE public."Table.With.Dot" ("Id" int PRIMARY KEY); -CREATE TABLE public."SimpleTableName" ("Id" int PRIMARY KEY); -CREATE TABLE public."JustTableName" ("Id" int PRIMARY KEY); - -CREATE TABLE db2."QuotedTableName" ("Id" int PRIMARY KEY); -CREATE TABLE db2."Table.With.Dot" ("Id" int PRIMARY KEY); -CREATE TABLE db2."SimpleTableName" ("Id" int PRIMARY KEY); -CREATE TABLE db2."JustTableName" ("Id" int PRIMARY KEY); - -CREATE TABLE "db2"."PrincipalTable" ( - "Id" int PRIMARY KEY, - "UC1" text, - "UC2" int, - "Index1" bit, - "Index2" bigint, - CONSTRAINT "UX" UNIQUE ("UC1", "UC2") -); - -CREATE INDEX "IX_COMPOSITE" ON "db2"."PrincipalTable" ("Index2", "Index1"); - -CREATE TABLE "db2"."DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeyId1" text, - "ForeignKeyId2" int, - FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "db2"."PrincipalTable"("UC1", "UC2") ON DELETE CASCADE -); -""", - [ - """ - "db.2"."QuotedTableName" - """, - """ - "db.2".SimpleTableName - """, - """ - public."Table.With.Dot" - """, - """ - public."SimpleTableName" - """, - """ - "JustTableName" - """ - ], - ["db2"], - dbModel => - { - var sequence = Assert.Single(dbModel.Sequences); - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("db2", sequence.Schema); - - Assert.Single(dbModel.Tables, t => t is { Schema: "db.2", Name: "QuotedTableName" }); - Assert.DoesNotContain(dbModel.Tables, t => t is { Schema: "db.2", Name: "Table.With.Dot" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "db.2", Name: "SimpleTableName" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "db.2", Name: "JustTableName" }); - - Assert.DoesNotContain(dbModel.Tables, t => t is { Schema: "public", Name: "QuotedTableName" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "public", Name: "Table.With.Dot" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "public", Name: "SimpleTableName" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "public", Name: "JustTableName" }); - - Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "QuotedTableName" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "Table.With.Dot" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "SimpleTableName" }); - Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "JustTableName" }); - - var principalTable = Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "PrincipalTable" }); - // ReSharper disable once PossibleNullReferenceException - Assert.NotNull(principalTable.PrimaryKey); - Assert.Single(principalTable.UniqueConstraints); - Assert.Single(principalTable.Indexes); - - var dependentTable = Assert.Single(dbModel.Tables, t => t is { Schema: "db2", Name: "DependentTable" }); - // ReSharper disable once PossibleNullReferenceException - Assert.Single(dependentTable.ForeignKeys); - }, - """ -DROP SEQUENCE public."Sequence"; -DROP SEQUENCE db2."Sequence"; - -DROP TABLE "db.2"."QuotedTableName"; -DROP TABLE "db.2"."Table.With.Dot"; -DROP TABLE "db.2"."SimpleTableName"; -DROP TABLE "db.2"."JustTableName"; - -DROP TABLE public."QuotedTableName"; -DROP TABLE public."Table.With.Dot"; -DROP TABLE public."SimpleTableName"; -DROP TABLE public."JustTableName"; - -DROP TABLE db2."QuotedTableName"; -DROP TABLE db2."Table.With.Dot"; -DROP TABLE db2."SimpleTableName"; -DROP TABLE db2."JustTableName"; -DROP TABLE db2."DependentTable"; -DROP TABLE db2."PrincipalTable"; -"""); - - #endregion - - #region Table - - [Fact] - public void Create_columns() - => Test( - """ -CREATE TABLE "Blogs" ( - "Id" int, - "Name" text NOT NULL -); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - Assert.Equal(2, table.Columns.Count); - Assert.All( - table.Columns, c => - { - Assert.Equal("public", c.Table.Schema); - Assert.Equal("Blogs", c.Table.Name); - }); - - Assert.Single(table.Columns, c => c.Name == "Id"); - Assert.Single(table.Columns, c => c.Name == "Name"); - }, - """ - DROP TABLE "Blogs" - """); - - [Fact] - public void Create_view_columns() - => Test( - """ -CREATE VIEW "BlogsView" AS SELECT 100::int AS "Id", ''::text AS "Name"; -""", - [], - [], - dbModel => - { - var table = Assert.IsType(dbModel.Tables.Single()); - - Assert.Equal(2, table.Columns.Count); - Assert.Null(table.PrimaryKey); - Assert.All( - table.Columns, c => - { - Assert.Equal("public", c.Table.Schema); - Assert.Equal("BlogsView", c.Table.Name); - }); - - Assert.Single(table.Columns, c => c.Name == "Id"); - Assert.Single(table.Columns, c => c.Name == "Name"); - }, - """DROP VIEW "BlogsView";"""); - - [Fact] - public void Create_materialized_view_columns() - => Test( - """ -CREATE MATERIALIZED VIEW "BlogsView" AS SELECT 100::int AS "Id", ''::text AS "Name"; -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - Assert.Equal(2, table.Columns.Count); - Assert.Null(table.PrimaryKey); - Assert.All( - table.Columns, c => - { - Assert.Equal("public", c.Table.Schema); - Assert.Equal("BlogsView", c.Table.Name); - }); - - Assert.Single(table.Columns, c => c.Name == "Id"); - Assert.Single(table.Columns, c => c.Name == "Name"); - }, - """DROP MATERIALIZED VIEW "BlogsView";"""); - - [Fact] - public void Create_primary_key() - => Test( - """ -CREATE TABLE "PrimaryKeyTable" ("Id" int PRIMARY KEY); -""", - [], - [], - dbModel => - { - var pk = dbModel.Tables.Single().PrimaryKey!; - - Assert.Equal("public", pk.Table!.Schema); - Assert.Equal("PrimaryKeyTable", pk.Table.Name); - Assert.StartsWith("PrimaryKeyTable_pkey", pk.Name); - Assert.Equal(["Id"], pk.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "PrimaryKeyTable" - """); - - [Fact] - public void Create_unique_constraints() - => Test( - """ -CREATE TABLE "UniqueConstraint" ( - "Id" int, - "Name" int Unique, - "IndexProperty" int, - "Unq1" int, - "Unq2" int, - UNIQUE ("Unq1", "Unq2") -); - -CREATE INDEX "IX_INDEX" on "UniqueConstraint" ("IndexProperty"); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - Assert.Equal(2, table.UniqueConstraints.Count); - - var firstConstraint = table.UniqueConstraints.Single(c => c.Columns.Count == 1); - Assert.Equal("public", firstConstraint.Table.Schema); - Assert.Equal("UniqueConstraint", firstConstraint.Table.Name); - //Assert.StartsWith("UQ__UniqueCo", uniqueConstraint.Name); - 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(["Unq1", "Unq2"], secondConstraint.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "UniqueConstraint" - """); - - [Fact] - public void Create_indexes() - => Test( - """ -CREATE TABLE "IndexTable" ( - "Id" int, - "Name" int, - "IndexProperty" int, - "ConstraintProperty" int, - UNIQUE ("ConstraintProperty") -); - -CREATE INDEX "IX_NAME" on "IndexTable" ("Name"); -CREATE INDEX "IX_INDEX" on "IndexTable" ("IndexProperty"); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - // Unique constraints should *not* be modelled as indices - Assert.Equal(2, table.Indexes.Count); - Assert.All( - table.Indexes, c => - { - Assert.Equal("public", c.Table!.Schema); - Assert.Equal("IndexTable", c.Table.Name); - }); - - Assert.Single(table.Indexes, c => c.Name == "IX_NAME"); - Assert.Single(table.Indexes, c => c.Name == "IX_INDEX"); - }, - """ - DROP TABLE "IndexTable" - """); - - [Fact] - public void Create_foreign_keys() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id" int PRIMARY KEY -); - -CREATE TABLE "FirstDependent" ( - "Id" int PRIMARY KEY, - "ForeignKeyId" int, - FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE -); - -CREATE TABLE "SecondDependent" ( - "Id" int PRIMARY KEY, - FOREIGN KEY ("Id") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION -); -""", - [], - [], - dbModel => - { - var firstFk = Assert.Single(dbModel.Tables.Single(t => t.Name == "FirstDependent").ForeignKeys); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", firstFk.Table.Schema); - Assert.Equal("FirstDependent", firstFk.Table.Name); - Assert.Equal("public", firstFk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", firstFk.PrincipalTable.Name); - 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); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", secondFk.Table.Schema); - Assert.Equal("SecondDependent", secondFk.Table.Name); - Assert.Equal("public", secondFk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", secondFk.PrincipalTable.Name); - 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); - }, - """ -DROP TABLE "SecondDependent"; -DROP TABLE "FirstDependent"; -DROP TABLE "PrincipalTable"; -"""); - - #endregion - - #region ColumnFacets - - [Fact] - public void Column_with_domain_assigns_underlying_store_type() - { - Fixture.TestStore.ExecuteNonQuery( - """ -CREATE DOMAIN public.text_domain AS text; -CREATE DOMAIN db2.text_domain AS int; -CREATE DOMAIN public.char_domain AS char(3); -"""); - - Test( - """ -CREATE TABLE domains ( - id int, - text_domain public.text_domain NULL, - char_domain public.char_domain NULL -) -""", - [], - [], - dbModel => - { - var textDomainColumn = Assert.Single(dbModel.Tables.Single().Columns, c => c.Name == "text_domain"); - Assert.Equal("text", textDomainColumn?.StoreType); - - var charDomainColumn = Assert.Single(dbModel.Tables.Single().Columns, c => c.Name == "char_domain"); - Assert.Equal("character(3)", charDomainColumn?.StoreType); - - var nonDomainColumn = Assert.Single(dbModel.Tables.Single().Columns, c => c.Name == "id"); - Assert.Equal("integer", nonDomainColumn?.StoreType); - }, - """ -DROP TABLE domains; -DROP DOMAIN public.text_domain; -DROP DOMAIN public.char_domain; -DROP DOMAIN db2.text_domain; -"""); - } - - // Note: in PostgreSQL decimal is simply an alias for numeric - [Fact] - public void Decimal_numeric_types_have_precision_scale() - => Test( - """ -CREATE TABLE "NumericColumns" ( - "Id" int, - "numericColumn" numeric NOT NULL, - "numeric152Column" numeric(15, 2) NOT NULL, - "numeric18Column" numeric(18) NOT NULL -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.Equal("numeric", columns.Single(c => c.Name == "numericColumn").StoreType); - 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" - """); - - [Fact] - public void Specific_max_length_are_add_to_store_type() - => Test( - """ -CREATE TABLE "LengthColumns" ( - "Id" int, - "char10Column" char(10) NULL, - "varchar66Column" varchar(66) NULL, - "bit111Column" bit(111) NULL, - "varbit123Column" varbit(123) NULL, - "varchar66ArrayColumn" varchar(66)[] NULL, - "varbit123ArrayColumn" varbit(123)[] NULL -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.Equal("character(10)", columns.Single(c => c.Name == "char10Column").StoreType); - Assert.Equal("character varying(66)", columns.Single(c => c.Name == "varchar66Column").StoreType); - Assert.Equal("bit(111)", columns.Single(c => c.Name == "bit111Column").StoreType); - Assert.Equal("bit varying(123)", columns.Single(c => c.Name == "varbit123Column").StoreType); - 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" - """); - - [Fact] - public void Datetime_types_have_precision_if_non_null_scale() - => Test( - """ -CREATE TABLE "LengthColumns" ( - "Id" int, - "time1Column" time(1) NULL, - "timetz2Column" timetz(2) NULL, - "timestamp3Column" timestamp(3) NULL, - "timestamptz4Column" timestamptz(4) NULL, - "interval5Column" interval(5) NULL -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.Equal("time(1) without time zone", columns.Single(c => c.Name == "time1Column").StoreType); - Assert.Equal("time(2) with time zone", columns.Single(c => c.Name == "timetz2Column").StoreType); - Assert.Equal("timestamp(3) without time zone", columns.Single(c => c.Name == "timestamp3Column").StoreType); - 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" - """); - - [Fact] - public void Store_types_without_any_facets() - => Test( - """ -CREATE TABLE "NoFacetTypes" ( - "Id" int, - "boolColumn" bool, - "byteaColumn" bytea, - "floatColumn" float4, - "doubleColumn" float8, - "decimalColumn" decimal, - "moneyColumn" money, - "guidColumn" uuid, - "shortColumn" int2, - "intColumn" int4, - "longColumn" int8, - "textColumn" text, - "jsonbColumn" jsonb, - "jsonColumn" json, - "timestampColumn" timestamp, - /* TODO: timestamptz */ - "intervalColumn" interval, - "timetzColumn" timetz, - "macaddrColumn" macaddr, - "inetColumn" inet, - "pointColumn" point, - "lineColumn" line, - "xidColumn" xid, - "textArrayColumn" text[] -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single(t => t.Name == "NoFacetTypes").Columns; - - Assert.Equal("boolean", columns.Single(c => c.Name == "boolColumn").StoreType); - Assert.Equal("bytea", columns.Single(c => c.Name == "byteaColumn").StoreType); - Assert.Equal("real", columns.Single(c => c.Name == "floatColumn").StoreType); - Assert.Equal("double precision", columns.Single(c => c.Name == "doubleColumn").StoreType); - Assert.Equal("numeric", columns.Single(c => c.Name == "decimalColumn").StoreType); - Assert.Equal("money", columns.Single(c => c.Name == "moneyColumn").StoreType); - Assert.Equal("uuid", columns.Single(c => c.Name == "guidColumn").StoreType); - Assert.Equal("smallint", columns.Single(c => c.Name == "shortColumn").StoreType); - Assert.Equal("integer", columns.Single(c => c.Name == "intColumn").StoreType); - Assert.Equal("bigint", columns.Single(c => c.Name == "longColumn").StoreType); - Assert.Equal("text", columns.Single(c => c.Name == "textColumn").StoreType); - Assert.Equal("jsonb", columns.Single(c => c.Name == "jsonbColumn").StoreType); - Assert.Equal("json", columns.Single(c => c.Name == "jsonColumn").StoreType); - Assert.Equal("timestamp without time zone", columns.Single(c => c.Name == "timestampColumn").StoreType); - Assert.Equal("interval", columns.Single(c => c.Name == "intervalColumn").StoreType); - Assert.Equal("time with time zone", columns.Single(c => c.Name == "timetzColumn").StoreType); - Assert.Equal("macaddr", columns.Single(c => c.Name == "macaddrColumn").StoreType); - Assert.Equal("inet", columns.Single(c => c.Name == "inetColumn").StoreType); - Assert.Equal("point", columns.Single(c => c.Name == "pointColumn").StoreType); - Assert.Equal("line", columns.Single(c => c.Name == "lineColumn").StoreType); - Assert.Equal("xid", columns.Single(c => c.Name == "xidColumn").StoreType); - Assert.Equal("text[]", columns.Single(c => c.Name == "textArrayColumn").StoreType); - }, - """ - DROP TABLE "NoFacetTypes" - """); - - [Fact] - public void Default_values_are_stored() - => Test( - """ -CREATE TABLE "DefaultValues" ( - "Id" int, - "FixedDefaultValue" timestamp NOT NULL DEFAULT ('1999-01-08') -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - Assert.Equal( - "'1999-01-08 00:00:00'::timestamp without time zone", - columns.Single(c => c.Name == "FixedDefaultValue").DefaultValueSql); - }, - """ - DROP TABLE "DefaultValues" - """); - - [ConditionalFact] - [MinimumPostgresVersion(12, 0)] - public void Computed_values_are_stored() - => Test( - """ -CREATE TABLE "ComputedValues" ( - "Id" int, - "A" int NOT NULL, - "B" int NOT NULL, - "SumOfAAndB" int GENERATED ALWAYS AS ("A" + "B") STORED -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - // Note that on-the-fly computed columns aren't (yet) supported by PostgreSQL, only stored/persisted - // columns. - var column = columns.Single(c => c.Name == "SumOfAAndB"); - Assert.Null(column.DefaultValueSql); - Assert.Equal("""("A" + "B")""", column.ComputedColumnSql); - Assert.True(column.IsStored); - }, - """ - DROP TABLE "ComputedValues" - """); - - [Fact] - public void ValueGenerated_is_set_for_default_and_serial_column() - => Test( - """ -CREATE TABLE "ValueGeneratedProperties" ( - "Id" SERIAL, - "NoValueGenerationColumn" text, - "FixedDefaultValue" timestamp NOT NULL DEFAULT ('1999-01-08') -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.Equal(ValueGenerated.OnAdd, columns.Single(c => c.Name == "Id").ValueGenerated); - Assert.Null(columns.Single(c => c.Name == "NoValueGenerationColumn").ValueGenerated); - Assert.Null(columns.Single(c => c.Name == "FixedDefaultValue").ValueGenerated); - }, - """ - DROP TABLE "ValueGeneratedProperties" - """); - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void ValueGenerated_is_set_for_identity_column() - => Test( - """ -CREATE TABLE "ValueGeneratedProperties" ( - "Id1" INT GENERATED ALWAYS AS IDENTITY, - "Id2" INT GENERATED BY DEFAULT AS IDENTITY -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - 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" - """); - - [ConditionalFact] - [MinimumPostgresVersion(12, 0)] - public void ValueGenerated_is_set_for_computed_column() - => Test( - """ -CREATE TABLE "ValueGeneratedProperties" ( - "Id" INT GENERATED ALWAYS AS IDENTITY, - "A" int NOT NULL, - "B" int NOT NULL, - "SumOfAAndB" int GENERATED ALWAYS AS ("A" + "B") STORED -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.Null(columns.Single(c => c.Name == "SumOfAAndB").ValueGenerated); - }, - """ - DROP TABLE "ValueGeneratedProperties" - """); - - [Fact] - public void Column_nullability_is_set() - => Test( - """ -CREATE TABLE "NullableColumns" ( - "Id" int, - "NullableInt" int NULL, - "NonNullableInt" int NOT NULL -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.True(columns.Single(c => c.Name == "NullableInt").IsNullable); - Assert.False(columns.Single(c => c.Name == "NonNullableInt").IsNullable); - }, - """ - DROP TABLE "NullableColumns" - """); - - [Fact] - public void Column_nullability_is_set_with_domain() - => Test( - """ -CREATE DOMAIN non_nullable_int AS int NOT NULL; - -CREATE TABLE "NullableColumnsDomain" ( - "Id" int, - "NullableInt" non_nullable_int NULL, - "NonNullString" non_nullable_int NOT NULL -) -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.False(columns.Single(c => c.Name == "NullableInt").IsNullable); - Assert.False(columns.Single(c => c.Name == "NonNullString").IsNullable); - }, - """ -DROP TABLE "NullableColumnsDomain"; -DROP DOMAIN non_nullable_int; -"""); - - [Fact] - public void System_columns_are_not_created() - => Test( - """ -CREATE TABLE "SystemColumnsTable" -( - "Id" int NOT NULL PRIMARY KEY -) -""", - [], - [], - dbModel => Assert.Single(dbModel.Tables.Single().Columns), - """ - DROP TABLE "SystemColumnsTable" - """); - - #endregion - - #region PrimaryKeyFacets - - [Fact] - public void Create_composite_primary_key() - => Test( - """ -CREATE TABLE "CompositePrimaryKeyTable" ( - "Id1" int, - "Id2" int, - PRIMARY KEY ("Id2", "Id1") -) -""", - [], - [], - dbModel => - { - var pk = dbModel.Tables.Single().PrimaryKey!; - - Assert.Equal("public", pk.Table!.Schema); - Assert.Equal("CompositePrimaryKeyTable", pk.Table.Name); - Assert.Equal(["Id2", "Id1"], pk.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "CompositePrimaryKeyTable" - """); - - [Fact] - public void Set_primary_key_name_from_index() - => Test( - """ -CREATE TABLE "PrimaryKeyName" ( - "Id1" int, - "Id2" int, - CONSTRAINT "MyPK" PRIMARY KEY ( "Id2" ) -) -""", - [], - [], - dbModel => - { - var pk = dbModel.Tables.Single().PrimaryKey!; - - Assert.Equal("public", pk.Table!.Schema); - Assert.Equal("PrimaryKeyName", pk.Table.Name); - Assert.StartsWith("MyPK", pk.Name); - Assert.Equal(["Id2"], pk.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "PrimaryKeyName" - """); - - #endregion - - #region UniqueConstraintFacets - - [Fact] - public void Create_composite_unique_constraint() - => Test( - """ -CREATE TABLE "CompositeUniqueConstraintTable" ( - "Id1" int, - "Id2" int, - CONSTRAINT "UX" UNIQUE ("Id2", "Id1") -); -""", - [], - [], - dbModel => - { - var uniqueConstraint = Assert.Single(dbModel.Tables.Single().UniqueConstraints); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", uniqueConstraint.Table.Schema); - Assert.Equal("CompositeUniqueConstraintTable", uniqueConstraint.Table.Name); - Assert.Equal("UX", uniqueConstraint.Name); - Assert.Equal(["Id2", "Id1"], uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "CompositeUniqueConstraintTable" - """); - - [Fact] - public void Set_unique_constraint_name_from_index() - => Test( - """ -CREATE TABLE "UniqueConstraintName" ( - "Id1" int, - "Id2" int, - CONSTRAINT "MyUC" UNIQUE ( "Id2" ) -); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - var uniqueConstraint = Assert.Single(table.UniqueConstraints); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", uniqueConstraint.Table.Schema); - Assert.Equal("UniqueConstraintName", uniqueConstraint.Table.Name); - Assert.Equal("MyUC", uniqueConstraint.Name); - Assert.Equal(["Id2"], uniqueConstraint.Columns.Select(ic => ic.Name).ToList()); - Assert.Empty(table.Indexes); - }, - """ - DROP TABLE "UniqueConstraintName" - """); - - #endregion - - #region IndexFacets - - [Fact] - public void Create_composite_index() - => Test( - """ -CREATE TABLE "CompositeIndexTable" ( - "Id1" int, - "Id2" int -); - -CREATE INDEX "IX_COMPOSITE" ON "CompositeIndexTable" ( "Id2", "Id1" ); -""", - [], - [], - dbModel => - { - var index = Assert.Single(dbModel.Tables.Single().Indexes); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", index.Table!.Schema); - Assert.Equal("CompositeIndexTable", index.Table.Name); - Assert.Equal("IX_COMPOSITE", index.Name); - Assert.Equal(["Id2", "Id1"], index.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "CompositeIndexTable" - """); - - [Fact] - public void Set_unique_true_for_unique_index() - => Test( - """ -CREATE TABLE "UniqueIndexTable" ( - "Id1" int, - "Id2" int -); - -CREATE UNIQUE INDEX "IX_UNIQUE" ON "UniqueIndexTable" ( "Id2" ); -""", - [], - [], - dbModel => - { - var index = Assert.Single(dbModel.Tables.Single().Indexes); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", index.Table!.Schema); - Assert.Equal("UniqueIndexTable", index.Table.Name); - Assert.Equal("IX_UNIQUE", index.Name); - Assert.True(index.IsUnique); - Assert.Null(index.Filter); - Assert.Equal(["Id2"], index.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "UniqueIndexTable" - """); - - [Fact] - public void Set_filter_for_filtered_index() - => Test( - """ -CREATE TABLE "FilteredIndexTable" ( - "Id1" int, - "Id2" int NULL -); - -CREATE UNIQUE INDEX "IX_UNIQUE" ON "FilteredIndexTable" ( "Id2" ) WHERE "Id2" > 10; -""", - [], - [], - dbModel => - { - var index = Assert.Single(dbModel.Tables.Single().Indexes); - - // ReSharper disable once PossibleNullReferenceException - 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(["Id2"], index.Columns.Select(ic => ic.Name).ToList()); - }, - """ - DROP TABLE "FilteredIndexTable" - """); - - #endregion - - #region ForeignKeyFacets - - [Fact] - public void Create_composite_foreign_key() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id1" int, - "Id2" int, - PRIMARY KEY ("Id1", "Id2") -); - -CREATE TABLE "DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeyId1" int, - "ForeignKeyId2" int, - FOREIGN KEY ("ForeignKeyId1", "ForeignKeyId2") REFERENCES "PrincipalTable"("Id1", "Id2") ON DELETE CASCADE -); -""", - [], - [], - dbModel => - { - var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", fk.Table.Schema); - Assert.Equal("DependentTable", fk.Table.Name); - Assert.Equal("public", fk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - 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); - }, - """ -DROP TABLE "DependentTable"; -DROP TABLE "PrincipalTable"; -"""); - - [Fact] - public void Create_multiple_foreign_key_in_same_table() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id" int PRIMARY KEY -); - -CREATE TABLE "AnotherPrincipalTable" ( - "Id" int PRIMARY KEY -); - -CREATE TABLE "DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeyId1" int, - "ForeignKeyId2" int, - FOREIGN KEY ("ForeignKeyId1") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE, - FOREIGN KEY ("ForeignKeyId2") REFERENCES "AnotherPrincipalTable"("Id") ON DELETE CASCADE -); -""", - [], - [], - dbModel => - { - var foreignKeys = dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys; - - Assert.Equal(2, foreignKeys.Count); - - var principalFk = Assert.Single(foreignKeys, f => f.PrincipalTable.Name == "PrincipalTable"); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", principalFk.Table.Schema); - Assert.Equal("DependentTable", principalFk.Table.Name); - Assert.Equal("public", principalFk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", principalFk.PrincipalTable.Name); - 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, f => f.PrincipalTable.Name == "AnotherPrincipalTable"); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", anotherPrincipalFk.Table.Schema); - Assert.Equal("DependentTable", anotherPrincipalFk.Table.Name); - Assert.Equal("public", anotherPrincipalFk.PrincipalTable.Schema); - Assert.Equal("AnotherPrincipalTable", anotherPrincipalFk.PrincipalTable.Name); - 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); - }, - """ -DROP TABLE "DependentTable"; -DROP TABLE "AnotherPrincipalTable"; -DROP TABLE "PrincipalTable"; -"""); - - [Fact] - public void Create_foreign_key_referencing_unique_constraint() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id1" int, - "Id2" int UNIQUE -); - -CREATE TABLE "DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeyId" int, - FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id2") ON DELETE CASCADE -); -""", - [], - [], - dbModel => - { - var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", fk.Table.Schema); - Assert.Equal("DependentTable", fk.Table.Name); - Assert.Equal("public", fk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - 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); - }, - """ -DROP TABLE "DependentTable"; -DROP TABLE "PrincipalTable"; -"""); - - [Fact] - public void Set_name_for_foreign_key() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id" int PRIMARY KEY -); - -CREATE TABLE "DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeyId" int, - CONSTRAINT "MYFK" FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE -); -""", - [], - [], - dbModel => - { - var fk = Assert.Single(dbModel.Tables.Single(t => t.Name == "DependentTable").ForeignKeys); - - // ReSharper disable once PossibleNullReferenceException - Assert.Equal("public", fk.Table.Schema); - Assert.Equal("DependentTable", fk.Table.Name); - Assert.Equal("public", fk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - 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); - }, - """ -DROP TABLE "DependentTable"; -DROP TABLE "PrincipalTable"; -"""); - - [Fact] - public void Set_referential_action_for_foreign_key() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id" int PRIMARY KEY -); - -CREATE TABLE "DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeySetNullId" int, - "ForeignKeyCascadeId" int, - "ForeignKeyNoActionId" int, - "ForeignKeyRestrictId" int, - "ForeignKeySetDefaultId" int, - FOREIGN KEY ("ForeignKeySetNullId") REFERENCES "PrincipalTable"("Id") ON DELETE SET NULL, - FOREIGN KEY ("ForeignKeyCascadeId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE, - FOREIGN KEY ("ForeignKeyNoActionId") REFERENCES "PrincipalTable"("Id") ON DELETE NO ACTION, - FOREIGN KEY ("ForeignKeyRestrictId") REFERENCES "PrincipalTable"("Id") ON DELETE RESTRICT, - FOREIGN KEY ("ForeignKeySetDefaultId") REFERENCES "PrincipalTable"("Id") ON DELETE SET DEFAULT -); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(t => t.Name == "DependentTable"); - - foreach (var fk in table.ForeignKeys) - { - Assert.Equal("public", fk.Table.Schema); - Assert.Equal("DependentTable", fk.Table.Name); - Assert.Equal("public", fk.PrincipalTable.Schema); - Assert.Equal("PrincipalTable", fk.PrincipalTable.Name); - Assert.Equal(["Id"], fk.PrincipalColumns.Select(ic => ic.Name).ToList()); - } - - Assert.Equal( - ReferentialAction.SetNull, table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeySetNullId").OnDelete); - Assert.Equal( - ReferentialAction.Cascade, table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeyCascadeId").OnDelete); - Assert.Equal( - ReferentialAction.NoAction, - table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeyNoActionId").OnDelete); - Assert.Equal( - ReferentialAction.Restrict, - table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeyRestrictId").OnDelete); - Assert.Equal( - ReferentialAction.SetDefault, - table.ForeignKeys.Single(fk => fk.Columns.Single().Name == "ForeignKeySetDefaultId").OnDelete); - }, - """ -DROP TABLE "DependentTable"; -DROP TABLE "PrincipalTable"; -"""); - - #endregion - - #region Warnings - - [Fact] - public void Warn_missing_schema() - => Test( - """ -CREATE TABLE "Blank" ("Id" int) -""", - [], - ["MySchema"], - dbModel => - { - Assert.Empty(dbModel.Tables); - - var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); - - Assert.Equal(NpgsqlResources.LogMissingSchema(new TestLogger()).EventId, Id); - Assert.Equal( - NpgsqlResources.LogMissingSchema(new TestLogger()).GenerateMessage("MySchema"), Message); - }, - """ - DROP TABLE "Blank" - """); - - [Fact] - public void Warn_missing_table() - => Test( - """ -CREATE TABLE "Blank" ("Id" int) -""", - ["MyTable"], - [], - dbModel => - { - Assert.Empty(dbModel.Tables); - - var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); - - Assert.Equal(NpgsqlResources.LogMissingTable(new TestLogger()).EventId, Id); - Assert.Equal( - NpgsqlResources.LogMissingTable(new TestLogger()).GenerateMessage("MyTable"), Message); - }, - """ - DROP TABLE "Blank" - """); - - [Fact] - public void Warn_missing_principal_table_for_foreign_key() - => Test( - """ -CREATE TABLE "PrincipalTable" ( - "Id" int PRIMARY KEY -); - -CREATE TABLE "DependentTable" ( - "Id" int PRIMARY KEY, - "ForeignKeyId" int, - CONSTRAINT "MYFK" FOREIGN KEY ("ForeignKeyId") REFERENCES "PrincipalTable"("Id") ON DELETE CASCADE -); -""", - ["DependentTable"], - [], - _ => - { - var (_, Id, Message, _, _) = Assert.Single(Fixture.ListLoggerFactory.Log, t => t.Level == LogLevel.Warning); - - Assert.Equal(NpgsqlResources.LogPrincipalTableNotInSelectionSet(new TestLogger()).EventId, Id); - Assert.Equal( - NpgsqlResources.LogPrincipalTableNotInSelectionSet(new TestLogger()).GenerateMessage( - "MYFK", "public.DependentTable", "public.PrincipalTable"), Message); - }, - """ -DROP TABLE "DependentTable"; -DROP TABLE "PrincipalTable"; -"""); - - #endregion - - #region PostgreSQL-specific - - [Fact] - public void SequenceSerial() - => Test( - """ -CREATE TABLE serial_sequence (id serial PRIMARY KEY); -CREATE TABLE "SerialSequence" ("Id" serial PRIMARY KEY); -CREATE SCHEMA my_schema; -CREATE TABLE my_schema.serial_sequence_in_schema (Id serial PRIMARY KEY); -CREATE TABLE my_schema."SerialSequenceInSchema" ("Id" serial PRIMARY KEY); -""", - [], - [], - dbModel => - { - // Sequences which belong to a serial column should not get reverse engineered as separate sequences - Assert.Empty(dbModel.Sequences); - - // Now make sure the field itself is properly reverse-engineered. - foreach (var column in dbModel.Tables.Select(t => t.Columns.Single())) - { - Assert.Equal(ValueGenerated.OnAdd, column.ValueGenerated); - Assert.Null(column.DefaultValueSql); - Assert.Equal( - NpgsqlValueGenerationStrategy.SerialColumn, - (NpgsqlValueGenerationStrategy?)column[NpgsqlAnnotationNames.ValueGenerationStrategy]); - } - }, - """ -DROP TABLE serial_sequence; -DROP TABLE "SerialSequence"; -DROP SCHEMA my_schema CASCADE -"""); - - [Fact] - public void SequenceNonSerial() - => Test( - """ -CREATE SEQUENCE "SomeSequence"; -CREATE TABLE "NonSerialSequence" ("Id" integer PRIMARY KEY DEFAULT nextval('"SomeSequence"')) -""", - [], - [], - dbModel => - { - var column = dbModel.Tables.Single().Columns.Single(); - 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. - Assert.Null(column.ValueGenerated); - - Assert.Single(dbModel.Sequences, s => s.Name == "SomeSequence"); - }, - """ -DROP TABLE "NonSerialSequence"; -DROP SEQUENCE "SomeSequence"; -"""); - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void Identity() - => Test( - """ -CREATE TABLE identity ( - id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - a int GENERATED ALWAYS AS IDENTITY, - b int GENERATED BY DEFAULT AS IDENTITY -) -""", - [], - [], - dbModel => - { - var idIdentityAlways = dbModel.Tables.Single().Columns.Single(c => c.Name == "id"); - Assert.Equal(ValueGenerated.OnAdd, idIdentityAlways.ValueGenerated); - Assert.Null(idIdentityAlways.DefaultValueSql); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, - (NpgsqlValueGenerationStrategy?)idIdentityAlways[NpgsqlAnnotationNames.ValueGenerationStrategy]); - - var identityAlways = dbModel.Tables.Single().Columns.Single(c => c.Name == "a"); - Assert.Equal(ValueGenerated.OnAdd, identityAlways.ValueGenerated); - Assert.Null(identityAlways.DefaultValueSql); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityAlwaysColumn, - (NpgsqlValueGenerationStrategy?)identityAlways[NpgsqlAnnotationNames.ValueGenerationStrategy]); - - var identityByDefault = dbModel.Tables.Single().Columns.Single(c => c.Name == "b"); - Assert.Equal(ValueGenerated.OnAdd, identityByDefault.ValueGenerated); - Assert.Null(identityByDefault.DefaultValueSql); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, - (NpgsqlValueGenerationStrategy?)identityByDefault[NpgsqlAnnotationNames.ValueGenerationStrategy]); - }, - "DROP TABLE identity"); - - [ConditionalFact] - [MinimumPostgresVersion(10, 0)] - public void Identity_with_sequence_options_all() - => Test( - """ -CREATE TABLE identity ( - with_options int GENERATED BY DEFAULT AS IDENTITY (START WITH 5 INCREMENT BY 2 MINVALUE 3 MAXVALUE 2000 CYCLE CACHE 10), - without_options int GENERATED BY DEFAULT AS IDENTITY, - bigint_without_options bigint GENERATED BY DEFAULT AS IDENTITY, - smallint_without_options smallint GENERATED BY DEFAULT AS IDENTITY -) -""", - [], - [], - dbModel => - { - var withOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "with_options"); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, - (NpgsqlValueGenerationStrategy?)withOptions[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Equal( - new IdentitySequenceOptionsData - { - StartValue = 5, - MinValue = 3, - MaxValue = 2000, - IncrementBy = 2, - IsCyclic = true, - NumbersToCache = 10 - }.Serialize(), withOptions[NpgsqlAnnotationNames.IdentityOptions]); - - var withoutOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "without_options"); - Assert.Equal("integer", withOptions.StoreType); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, - (NpgsqlValueGenerationStrategy?)withoutOptions[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(withoutOptions[NpgsqlAnnotationNames.IdentityOptions]); - - var bigintWithoutOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "bigint_without_options"); - Assert.Equal("bigint", bigintWithoutOptions.StoreType); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, - (NpgsqlValueGenerationStrategy?)bigintWithoutOptions[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(bigintWithoutOptions[NpgsqlAnnotationNames.IdentityOptions]); - - var smallintWithoutOptions = dbModel.Tables.Single().Columns.Single(c => c.Name == "smallint_without_options"); - Assert.Equal("smallint", smallintWithoutOptions.StoreType); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, - (NpgsqlValueGenerationStrategy?)smallintWithoutOptions[NpgsqlAnnotationNames.ValueGenerationStrategy]); - Assert.Null(smallintWithoutOptions[NpgsqlAnnotationNames.IdentityOptions]); - }, - "DROP TABLE identity"); - - [Fact] - public void Column_collation_is_set() - => Test( - """ -CREATE TABLE columns_with_collation ( - id int, - default_collation TEXT, - non_default_collation TEXT COLLATE "POSIX" -); -""", - [], - [], - dbModel => - { - var columns = dbModel.Tables.Single().Columns; - - Assert.Null(columns.Single(c => c.Name == "default_collation").Collation); - Assert.Equal("POSIX", columns.Single(c => c.Name == "non_default_collation").Collation); - }, - @"DROP TABLE columns_with_collation"); - - [ConditionalFact] - public void Default_database_collation_is_not_scaffolded() - => Test( - @"-- Empty database", - [], - [], - dbModel => Assert.Null(dbModel.Collation), - @""); - - [Fact] - public void Index_method() - => Test( - """ -CREATE TABLE "IndexMethod" (a int, b int); -CREATE INDEX ix_a ON "IndexMethod" USING hash (a); -CREATE INDEX ix_b ON "IndexMethod" (b); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - Assert.Equal(2, table.Indexes.Count); - - var methodIndex = table.Indexes.Single(i => i.Name == "ix_a"); - Assert.Equal("hash", methodIndex[NpgsqlAnnotationNames.IndexMethod]); - - // It's cleaner to always output the index method on the database model, - // even when it's btree (the default); - // NpgsqlAnnotationCodeGenerator can then omit it as by-convention. - // However, because of https://github.com/aspnet/EntityFrameworkCore/issues/11846 we omit - // the annotation from the model entirely. - var noMethodIndex = table.Indexes.Single(i => i.Name == "ix_b"); - Assert.Null(noMethodIndex.FindAnnotation(NpgsqlAnnotationNames.IndexMethod)); - //Assert.Equal("btree", noMethodIndex.FindAnnotation(NpgsqlAnnotationNames.IndexMethod).Value); - }, - """ - DROP TABLE "IndexMethod" - """); - - [Fact] - public void Index_operators() - => Test( - """ -CREATE TABLE "IndexOperators" (a text, b text); -CREATE INDEX ix_with ON "IndexOperators" (a, b varchar_pattern_ops); -CREATE INDEX ix_without ON "IndexOperators" (a, b); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); - Assert.Equal(new[] { null, "varchar_pattern_ops" }, indexWith[NpgsqlAnnotationNames.IndexOperators]); - - var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); - Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexOperators)); - }, - """ - DROP TABLE "IndexOperators" - """); - - [Fact] - public void Index_collation() - => Test( - """ -CREATE TABLE "IndexCollation" (a text, b text); -CREATE INDEX ix_with ON "IndexCollation" (a, b COLLATE "POSIX"); -CREATE INDEX ix_without ON "IndexCollation" (a, b); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); - Assert.Equal(new[] { null, "POSIX" }, indexWith[RelationalAnnotationNames.Collation]); - - var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); - Assert.Null(indexWithout.FindAnnotation(RelationalAnnotationNames.Collation)); - }, - """ - DROP TABLE "IndexCollation" - """); - - [Theory] - [InlineData("gin", new bool[0])] - [InlineData("gist", new bool[0])] - [InlineData("hash", new bool[0])] - [InlineData("brin", new bool[0])] - [InlineData("btree", new[] { false, true })] - public void Index_IsDescending(string method, bool[] expected) - => Test( - """ -CREATE TABLE "IndexSortOrder" (a text, b text, c tsvector); -CREATE INDEX ix_gin ON "IndexSortOrder" USING gin (c); -CREATE INDEX ix_gist ON "IndexSortOrder" USING gist (c); -CREATE INDEX ix_hash ON "IndexSortOrder" USING hash (a); -CREATE INDEX ix_brin ON "IndexSortOrder" USING brin (a); -CREATE INDEX ix_btree ON "IndexSortOrder" USING btree (a ASC, b DESC); -CREATE INDEX ix_without ON "IndexSortOrder" (a, b); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - var indexWith = table.Indexes.Single(i => i.Name == $"ix_{method}"); - // Assert.True(indexWith.IsDescending.SequenceEqual(expected)); - Assert.Equal(expected, indexWith.IsDescending); - - var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); - Assert.Equal([false, false], indexWithout.IsDescending); - }, - """ - DROP TABLE "IndexSortOrder" - """); - - [Fact] - public void Index_null_sort_order() - => Test( - """ -CREATE TABLE "IndexNullSortOrder" (a text, b text); -CREATE INDEX ix_with ON "IndexNullSortOrder" (a NULLS FIRST, b DESC NULLS LAST); -CREATE INDEX ix_without ON "IndexNullSortOrder" (a, b); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); - Assert.Equal( - new[] { NullSortOrder.NullsFirst, NullSortOrder.NullsLast }, - indexWith[NpgsqlAnnotationNames.IndexNullSortOrder]); - - var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); - Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexNullSortOrder)); - }, - """ - DROP TABLE "IndexNullSortOrder" - """); - - [ConditionalFact] - [MinimumPostgresVersion(11, 0)] - public void Index_covering() - => Test( - """ -CREATE TABLE "IndexCovering" (a text, b text, c text); -CREATE INDEX ix_with ON "IndexCovering" (a) INCLUDE (b, c); -CREATE INDEX ix_without ON "IndexCovering" (a, b, c); -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); - Assert.Equal("a", indexWith.Columns.Single().Name); - // Scaffolding included/covered properties is currently blocked, see #2194 - Assert.Null(indexWith.FindAnnotation(NpgsqlAnnotationNames.IndexInclude)); - // Assert.Equal(new[] { "b", "c" }, indexWith.FindAnnotation(NpgsqlAnnotationNames.IndexInclude).Value); - - var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); - Assert.Equal(new[] { "a", "b", "c" }, indexWithout.Columns.Select(i => i.Name).ToArray()); - Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexInclude)); - }, - """ - DROP TABLE "IndexCovering" - """); - - [ConditionalFact] - [MinimumPostgresVersion(15, 0)] - public void Index_are_nulls_distinct() - => Test( - """ -CREATE TABLE "IndexNullsDistinct" (a text); -CREATE INDEX "IX_NullsDistinct" ON "IndexNullsDistinct" (a); -CREATE INDEX "IX_NullsNotDistinct" ON "IndexNullsDistinct" (a) NULLS NOT DISTINCT; -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - - Assert.Null( - Assert.Single(table.Indexes, i => i.Name == "IX_NullsDistinct")[NpgsqlAnnotationNames.NullsDistinct]); - - Assert.Equal( - false, - Assert.Single(table.Indexes, i => i.Name == "IX_NullsNotDistinct")[NpgsqlAnnotationNames.NullsDistinct]); - }, - """ - DROP TABLE "IndexNullsDistinct" - """); - - [Fact] - public void Comments() - => Test( - """ -CREATE TABLE comment (a int); -COMMENT ON TABLE comment IS 'table comment'; -COMMENT ON COLUMN comment.a IS 'column comment' -""", - [], - [], - dbModel => - { - var table = dbModel.Tables.Single(); - Assert.Equal("table comment", table.Comment); - Assert.Equal("column comment", table.Columns.Single().Comment); - }, - "DROP TABLE comment"); - - [ConditionalFact] - [MinimumPostgresVersion(11, 0)] - public void Sequence_types() - => Test( - """ -CREATE SEQUENCE "SmallIntSequence" AS smallint; -CREATE SEQUENCE "IntSequence" AS int; -CREATE SEQUENCE "BigIntSequence" AS bigint; -""", - [], - [], - dbModel => - { - var smallSequence = dbModel.Sequences.Single(s => s.Name == "SmallIntSequence"); - Assert.Equal("smallint", smallSequence.StoreType); - var intSequence = dbModel.Sequences.Single(s => s.Name == "IntSequence"); - Assert.Equal("integer", intSequence.StoreType); - var bigSequence = dbModel.Sequences.Single(s => s.Name == "BigIntSequence"); - Assert.Equal("bigint", bigSequence.StoreType); - }, - """ -DROP SEQUENCE "SmallIntSequence"; -DROP SEQUENCE "IntSequence"; -DROP SEQUENCE "BigIntSequence"; -"""); - - [Fact] - public void Dropped_columns() - => Test( - """ -CREATE TABLE foo (id int PRIMARY KEY); -ALTER TABLE foo DROP COLUMN id; -ALTER TABLE foo ADD COLUMN id2 int PRIMARY KEY; -""", - [], - [], - dbModel => - { - Assert.Single(dbModel.Tables.Single().Columns); - }, - "DROP TABLE foo"); - - [Fact] - public void Postgres_extensions() - => Test( - """ -DROP EXTENSION IF EXISTS postgis; -CREATE EXTENSION hstore; -CREATE EXTENSION pgcrypto SCHEMA db2; -""", - [], - [], - dbModel => - { - var extensions = dbModel.GetPostgresExtensions(); - Assert.Collection( - extensions.OrderBy(e => e.Name), - e => - { - Assert.Equal("hstore", e.Name); - Assert.Equal("public", e.Schema); - }, - e => - { - Assert.Equal("pgcrypto", e.Name); - Assert.Equal("db2", e.Schema); - }); - }, - "DROP EXTENSION hstore; DROP EXTENSION pgcrypto"); - - [Fact] - public void Enums() - => Test( - """ -CREATE TYPE mood AS ENUM ('happy', 'sad'); -CREATE TYPE db2.mood AS ENUM ('excited', 'depressed'); -CREATE TABLE foo (mood mood UNIQUE); -""", - [], - [], - dbModel => - { - var enums = dbModel.GetPostgresEnums(); - Assert.Equal(2, enums.Count); - - var mood = enums.Single(e => e.Schema is null); - Assert.Equal("mood", mood.Name); - Assert.Equal(["happy", "sad"], mood.Labels); - - var mood2 = enums.Single(e => e.Schema == "db2"); - Assert.Equal("mood", mood2.Name); - Assert.Equal(["excited", "depressed"], mood2.Labels); - - var table = Assert.Single(dbModel.Tables); - Assert.NotNull(table); - - // Enum columns are left out of the model for now (a warning is logged). - Assert.Empty(table.Columns); - // Constraints and indexes over enum columns also need to be left out - Assert.Empty(table.UniqueConstraints); - Assert.Empty(table.Indexes); - }, - """ -DROP TABLE foo; -DROP TYPE mood; -DROP TYPE db2.mood; -"""); - - [Fact] - public void Bug453() - => Test( - """ -CREATE TYPE mood AS ENUM ('happy', 'sad'); -CREATE TABLE foo (mood mood, some_num int UNIQUE); -CREATE TABLE bar (foreign_key int REFERENCES foo(some_num)); -""", - [], - [], - // 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), - """ -DROP TABLE bar; -DROP TABLE foo; -DROP TYPE mood; -"""); - - [Fact] - public void Column_default_type_names_are_scaffolded() - => Test( - """ -CREATE TABLE column_types ( - smallint smallint, - integer integer, - bigint bigint, - real real, - "double precision" double precision, - money money, - numeric numeric, - boolean boolean, - bytea bytea, - uuid uuid, - text text, - jsonb jsonb, - json json, - "character varying" character varying, - "character(1)" character, - "character(2)" character(2), - "timestamp without time zone" timestamp, - "timestamp with time zone" timestamptz, - "time without time zone" time, - "time with time zone" timetz, - interval interval, - macaddr macaddr, - inet inet, - "bit(1)" bit, - "bit varying" varbit, - point point, - line line -) -""", - [], - [], - dbModel => - { - var options = new NpgsqlSingletonOptions(); - options.Initialize(new DbContextOptionsBuilder().Options); - - var typeMappingSource = new NpgsqlTypeMappingSource( - new TypeMappingSourceDependencies( - new ValueConverterSelector(new ValueConverterSelectorDependencies()), - new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - [] - ), - new RelationalTypeMappingSourceDependencies([]), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - options); - - foreach (var column in dbModel.Tables.Single().Columns) - { - Assert.Equal(column.Name, column.StoreType); - Assert.Equal( - column.StoreType, - typeMappingSource.FindMapping(column.StoreType!)!.StoreType - ); - } - }, - "DROP TABLE column_types"); - - [ConditionalFact] - [RequiresPostgis] - public void System_tables_are_ignored() - => Test( - """ -DROP EXTENSION IF EXISTS postgis; -CREATE EXTENSION postgis; -""", - [], - [], - dbModel => Assert.Empty(dbModel.Tables), - "DROP EXTENSION postgis"); - - #endregion - - private void Test( - string createSql, - IEnumerable tables, - IEnumerable schemas, - Action asserter, - string? cleanupSql) - { - Fixture.TestStore.ExecuteNonQuery(createSql); - - try - { - var databaseModelFactory = new NpgsqlDatabaseModelFactory( - new DiagnosticsLogger( - Fixture.ListLoggerFactory, - new LoggingOptions(), - new DiagnosticListener("Fake"), - new NpgsqlLoggingDefinitions(), - new NullDbContextLogger())); - - var databaseModel = databaseModelFactory.Create( - Fixture.TestStore.ConnectionString, - new DatabaseModelFactoryOptions(tables, schemas)); - Assert.NotNull(databaseModel); - asserter(databaseModel); - } - finally - { - if (!string.IsNullOrEmpty(cleanupSql)) - { - Fixture.TestStore.ExecuteNonQuery(cleanupSql); - } - } - } - - public class NpgsqlDatabaseModelFixture : SharedStoreFixtureBase - { - protected override string StoreName { get; } = nameof(NpgsqlDatabaseModelFactoryTest); - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public new NpgsqlTestStore TestStore - => (NpgsqlTestStore)base.TestStore; - - 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" - """); - } - - protected override bool ShouldLogCategory(string logCategory) - => logCategory == DbLoggerCategory.Scaffolding.Name; - } -} diff --git a/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs deleted file mode 100644 index 9a8d29e32e..0000000000 --- a/test/EFCore.PG.FunctionalTests/SeedingNpgsqlTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class SeedingNpgsqlTest : SeedingTestBase -{ - protected override TestStore TestStore - => NpgsqlTestStore.Create("SeedingTest"); - - protected override SeedingContext CreateContextWithEmptyDatabase(string testId) - => new SeedingNpgsqlContext(testId); - - protected class SeedingNpgsqlContext(string testId) : SeedingContext(testId) - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(NpgsqlTestStore.CreateConnectionString($"Seeds{TestId}")); - } -} diff --git a/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs deleted file mode 100644 index ce9a2d6751..0000000000 --- a/test/EFCore.PG.FunctionalTests/SerializationNpgsqlTest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class SerializationNpgsqlTest(F1BytesNpgsqlFixture fixture) : SerializationTestBase(fixture); diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs deleted file mode 100644 index 8b255aa533..0000000000 --- a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore; - -public class SpatialNpgsqlFixture : SpatialFixtureBase -{ - // 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 - => new NpgsqlTestStoreFactory(useConnectionString: true); - - protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection) - .AddEntityFrameworkNpgsqlNetTopologySuite(); - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder) - .UseNetTopologySuite() - .SetPostgresVersion(TestEnvironment.PostgresVersion); - - return optionsBuilder; - } - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("uuid-ossp"); - } -} diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs deleted file mode 100644 index aabac3ab23..0000000000 --- a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -[RequiresPostgis] -public class SpatialNpgsqlTest(SpatialNpgsqlFixture fixture) : SpatialTestBase(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 deleted file mode 100644 index 51391ef591..0000000000 --- a/test/EFCore.PG.FunctionalTests/StoreGeneratedFixupNpgsqlTest.cs +++ /dev/null @@ -1,186 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class StoreGeneratedFixupNpgsqlTest(StoreGeneratedFixupNpgsqlTest.StoreGeneratedFixupNpgsqlFixture fixture) - : StoreGeneratedFixupRelationalTestBase(fixture) -{ - [Fact] - public Task Temp_values_are_replaced_on_save() - => ExecuteWithStrategyInTransactionAsync( - async context => - { - var entry = context.Add(new TestTemp()); - - Assert.True(entry.Property(e => e.Id).IsTemporary); - Assert.False(entry.Property(e => e.NotId).IsTemporary); - - var tempValue = entry.Property(e => e.Id).CurrentValue; - - await context.SaveChangesAsync(); - - Assert.False(entry.Property(e => e.Id).IsTemporary); - Assert.NotEqual(tempValue, entry.Property(e => e.Id).CurrentValue); - }); - - protected override void MarkIdsTemporary(DbContext context, object dependent, object principal) - { - var entry = context.Entry(dependent); - entry.Property("Id1").IsTemporary = true; - entry.Property("Id2").IsTemporary = true; - - foreach (var property in entry.Properties) - { - if (property.Metadata.IsForeignKey()) - { - property.IsTemporary = true; - } - } - - entry = context.Entry(principal); - entry.Property("Id1").IsTemporary = true; - entry.Property("Id2").IsTemporary = true; - } - - protected override void MarkIdsTemporary(DbContext context, object game, object level, object item) - { - var entry = context.Entry(game); - entry.Property("Id").IsTemporary = true; - - entry = context.Entry(item); - entry.Property("Id").IsTemporary = true; - } - - protected override bool EnforcesFKs - => true; - - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public class StoreGeneratedFixupNpgsqlFixture : StoreGeneratedFixupRelationalFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("uuid-ossp"); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Id1).ValueGeneratedOnAdd(); - b.Property(e => e.Id2).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); - }); - - modelBuilder.Entity(b => { b.Property(e => e.Id).ValueGeneratedOnAdd(); }); - - modelBuilder.Entity(b => { b.Property(e => e.Id).ValueGeneratedOnAdd().HasDefaultValueSql("uuid_generate_v4()"); }); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs deleted file mode 100644 index d927da61fc..0000000000 --- a/test/EFCore.PG.FunctionalTests/StoreGeneratedNpgsqlTest.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class StoreGeneratedNpgsqlTest(StoreGeneratedNpgsqlTest.StoreGeneratedNpgsqlFixture fixture) - : StoreGeneratedTestBase(fixture) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public class StoreGeneratedNpgsqlFixture : StoreGeneratedFixtureBase - { - protected override string StoreName { get; } = "StoreGeneratedTest"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => builder - .EnableSensitiveDataLogging() - .ConfigureWarnings( - b => b.Default(WarningBehavior.Throw) - .Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning) - .Ignore(RelationalEventId.BoolWithDefaultWarning)); - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - modelBuilder.Entity( - b => - { - b.Property(e => e.Id).UseSerialColumn(); - b.Property(e => e.Identity).HasDefaultValue("Banana Joe"); - b.Property(e => e.IdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); - b.Property(e => e.IdentityReadOnlyAfterSave).HasDefaultValue("Anton"); - b.Property(e => e.AlwaysIdentity).HasDefaultValue("Banana Joe"); - b.Property(e => e.AlwaysIdentityReadOnlyBeforeSave).HasDefaultValue("Doughnut Sheriff"); - b.Property(e => e.AlwaysIdentityReadOnlyAfterSave).HasDefaultValue("Anton"); - b.Property(e => e.Computed).HasDefaultValue("Alan"); - b.Property(e => e.ComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); - b.Property(e => e.ComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); - b.Property(e => e.AlwaysComputed).HasDefaultValue("Alan"); - b.Property(e => e.AlwaysComputedReadOnlyBeforeSave).HasDefaultValue("Carmen"); - b.Property(e => e.AlwaysComputedReadOnlyAfterSave).HasDefaultValue("Tina Rex"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.OnAdd).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddUseBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddThrowBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddUseBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); - - b.Property(e => e.OnAddOrUpdate).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnAddOrUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); - - b.Property(e => e.OnUpdate).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateUseBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateIgnoreBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateThrowBeforeUseAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateUseBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateIgnoreBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateThrowBeforeIgnoreAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateUseBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateIgnoreBeforeThrowAfter).HasDefaultValue("Rabbit"); - b.Property(e => e.OnUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); - }); - - // In StoreGeneratedSqlServerTest the following are defined with HasComputedValueSql, but that's - // not supported by PostgreSQL - modelBuilder.Entity( - b => - { - b.Property(e => e.NullableAsNonNullable).HasDefaultValueSql("1"); - b.Property(e => e.NonNullableAsNullable).HasDefaultValueSql("1"); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.TrueDefault).HasDefaultValue(true); - b.Property(e => e.NonZeroDefault).HasDefaultValue(-1); - b.Property(e => e.FalseDefault).HasDefaultValue(false); - b.Property(e => e.ZeroDefault).HasDefaultValue(0); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); - b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); - b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); - b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); - }); - - modelBuilder.Entity( - b => - { - b.Property(e => e.NullableBackedBoolTrueDefault).HasDefaultValue(true); - b.Property(e => e.NullableBackedIntNonZeroDefault).HasDefaultValue(-1); - b.Property(e => e.NullableBackedBoolFalseDefault).HasDefaultValue(false); - b.Property(e => e.NullableBackedIntZeroDefault).HasDefaultValue(0); - }); - - modelBuilder.Entity().Property(e => e.HasTemp).HasDefaultValue(777); - - modelBuilder.Entity().Property(e => e.Id).UseIdentityColumn(); - - base.OnModelCreating(modelBuilder, context); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs deleted file mode 100644 index 7c35b9b244..0000000000 --- a/test/EFCore.PG.FunctionalTests/TPTTableSplittingNpgsqlTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class TPTTableSplittingNpgsqlTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) - : TPTTableSplittingTestBase(fixture, testOutputHelper) -{ - public override Task Can_insert_dependent_with_just_one_parent() - // This scenario is not valid for TPT - => Task.CompletedTask; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs deleted file mode 100644 index ca7b8f12c5..0000000000 --- a/test/EFCore.PG.FunctionalTests/TableSplittingNpgsqlTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.TransportationModel; - -namespace Microsoft.EntityFrameworkCore; - -[MinimumPostgresVersion(12, 0)] // Test suite uses computed columns -public class TableSplittingNpgsqlTest(NonSharedFixture fixture, ITestOutputHelper testOutputHelper) - : TableSplittingTestBase(fixture, testOutputHelper) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override async Task ExecuteUpdate_works_for_table_sharing(bool async) - { - await base.ExecuteUpdate_works_for_table_sharing(async); - - AssertSql( - """ -@p='1' - -UPDATE "Vehicles" AS v -SET "SeatingCapacity" = @p -""", - // - """ -SELECT NOT EXISTS ( - SELECT 1 - FROM "Vehicles" AS v - WHERE v."SeatingCapacity" <> 1) -"""); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity().ToTable("Vehicles") - .Property(e => e.Computed).HasComputedColumnSql("1", stored: true); - } -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs deleted file mode 100644 index 73f624818e..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Data.Common; -using System.Text; -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public class NpgsqlDatabaseCleaner : RelationalDatabaseCleaner -{ - private readonly NpgsqlSqlGenerationHelper _sqlGenerationHelper = new(new RelationalSqlGenerationHelperDependencies()); - - protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFactory loggerFactory) - => new NpgsqlDatabaseModelFactory( - new DiagnosticsLogger( - loggerFactory, - new LoggingOptions(), - new DiagnosticListener("Fake"), - new NpgsqlLoggingDefinitions(), - new NullDbContextLogger())); - - protected override bool AcceptIndex(DatabaseIndex index) - => false; - - public override void Clean(DatabaseFacade facade) - { - // The following is somewhat hacky - // PostGIS creates some system tables (e.g. spatial_ref_sys) which can't be dropped until the extension - // is dropped. But our tests create some user tables which depend on PostGIS. So we clean out PostGIS - // and all tables that depend on it (CASCADE) before the database model is built. - var creator = facade.GetService(); - var connection = facade.GetService(); - if (creator.Exists()) - { - connection.Open(); - try - { - var conn = (NpgsqlConnection)connection.DbConnection; - DropExtensions(conn); - DropTypes(conn); - DropFunctions(conn); - DropCollations(conn); - } - finally - { - connection.Close(); - } - } - - base.Clean(facade); - } - - private void DropExtensions(NpgsqlConnection conn) - { - 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)) - { - using var reader = cmd.ExecuteReader(); - extensions = reader.Cast().Select(r => r.GetString(0)).ToList(); - } - - if (extensions.Any()) - { - var dropExtensionsSql = string.Join("", extensions.Select(e => $"DROP EXTENSION \"{e}\" CASCADE;")); - using var cmd = new NpgsqlCommand(dropExtensionsSql, conn); - cmd.ExecuteNonQuery(); - } - } - - /// - /// Drop user-defined ranges and enums, cascading to all tables which depend on them - /// - private void DropTypes(NpgsqlConnection conn) - { - 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' -"""; - - (string Schema, string Name)[] userDefinedTypes; - using (var cmd = new NpgsqlCommand(getUserDefinedRangesEnums, conn)) - { - using var reader = cmd.ExecuteReader(); - userDefinedTypes = reader.Cast().Select(r => (r.GetString(0), r.GetString(1))).ToArray(); - } - - if (userDefinedTypes.Any()) - { - var dropTypes = string.Concat(userDefinedTypes.Select(t => $"""DROP TYPE "{t.Schema}"."{t.Name}" CASCADE;""")); - using var cmd = new NpgsqlCommand(dropTypes, conn); - cmd.ExecuteNonQuery(); - } - } - - /// - /// Drop all user-defined functions and procedures - /// - private void DropFunctions(NpgsqlConnection conn) - { - 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 - 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'); -"""; - - string dropSql; - using (var cmd = new NpgsqlCommand(getUserDefinedFunctions, conn)) - { - using var reader = cmd.ExecuteReader(); - dropSql = string.Join("", reader.Cast().Select(r => r.GetString(0))); - } - - if (dropSql != "") - { - using var cmd = new NpgsqlCommand(dropSql, conn); - cmd.ExecuteNonQuery(); - } - } - - private void DropCollations(NpgsqlConnection conn) - { - if (conn.PostgreSqlVersion < new Version(9, 1)) - { - return; - } - - 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 nspname <> 'pg_catalog'; -"""; - - (string Schema, string Name)[] userDefinedTypes; - using (var cmd = new NpgsqlCommand(getUserCollations, conn)) - { - using var reader = cmd.ExecuteReader(); - userDefinedTypes = reader.Cast().Select(r => (r.GetString(0), r.GetString(1))).ToArray(); - } - - if (userDefinedTypes.Any()) - { - var dropTypes = string.Concat(userDefinedTypes.Select(t => $"""DROP COLLATION "{t.Schema}"."{t.Name}" CASCADE;""")); - using var cmd = new NpgsqlCommand(dropTypes, conn); - cmd.ExecuteNonQuery(); - } - } - - protected override string BuildCustomSql(DatabaseModel databaseModel) - // Some extensions create tables (e.g. PostGIS), so we must drop them first. - => databaseModel.GetPostgresExtensions() - .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) - .Aggregate( - new StringBuilder(), - (builder, s) => builder.Append("DROP EXTENSION ").Append(s).Append(";"), - builder => builder.ToString()); - - protected override string BuildCustomEndingSql(DatabaseModel databaseModel) - => databaseModel.GetPostgresEnums() - .Select(e => _sqlGenerationHelper.DelimitIdentifier(e.Name, e.Schema)) - .Aggregate( - new StringBuilder(), - (builder, s) => builder.Append("DROP TYPE ").Append(s).Append(" CASCADE;"), - builder => builder.ToString()); -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseFacadeExtensions.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseFacadeExtensions.cs deleted file mode 100644 index 838c27ab42..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseFacadeExtensions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public static class NpgsqlDatabaseFacadeExtensions -{ - public static void EnsureClean(this DatabaseFacade databaseFacade) - => databaseFacade.CreateExecutionStrategy() - .Execute(databaseFacade, database => new NpgsqlDatabaseCleaner().Clean(database)); -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDbContextOptionsBuilderExtensions.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDbContextOptionsBuilderExtensions.cs deleted file mode 100644 index 107d33c8c8..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDbContextOptionsBuilderExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public static class NpgsqlDbContextOptionsBuilderExtensions -{ - public static NpgsqlDbContextOptionsBuilder ApplyConfiguration(this NpgsqlDbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); - - optionsBuilder.CommandTimeout(NpgsqlTestStore.CommandTimeout); - - return optionsBuilder; - } -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs deleted file mode 100644 index f2f2e43bad..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlNorthwindTestStoreFactory.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public class NpgsqlNorthwindTestStoreFactory : NpgsqlTestStoreFactory -{ - public const string Name = "Northwind"; - public static readonly string NorthwindConnectionString = NpgsqlTestStore.CreateConnectionString(Name); - public static new NpgsqlNorthwindTestStoreFactory Instance { get; } = new(); - - static NpgsqlNorthwindTestStoreFactory() - { - // TODO: Switch to using NpgsqlDataSource -#pragma warning disable CS0618 // Type or member is obsolete - NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson(); - NpgsqlConnection.GlobalTypeMapper.EnableRecordsAsTuples(); -#pragma warning restore CS0618 // Type or member is obsolete - } - - protected NpgsqlNorthwindTestStoreFactory() - { - } - - public override TestStore GetOrCreate(string storeName) - => NpgsqlTestStore.GetOrCreate( - 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 deleted file mode 100644 index f81f948601..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlPrecompiledQueryTestHelpers.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.CodeAnalysis; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Microsoft.EntityFrameworkCore.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/NpgsqlTestHelpers.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestHelpers.cs deleted file mode 100644 index 6b2751f733..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestHelpers.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal; - -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public class NpgsqlTestHelpers : RelationalTestHelpers -{ - protected NpgsqlTestHelpers() { } - - public static NpgsqlTestHelpers Instance { get; } = new(); - - public override IServiceCollection AddProviderServices(IServiceCollection services) - => services.AddEntityFrameworkNpgsql(); - - public override DbContextOptionsBuilder UseProviderOptions(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql(new NpgsqlConnection("Host=localhost;Database=DummyDatabase")); - - public override LoggingDefinitions LoggingDefinitions { get; } = new NpgsqlLoggingDefinitions(); -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs deleted file mode 100644 index 172704d342..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs +++ /dev/null @@ -1,441 +0,0 @@ -using System.Data; -using System.Data.Common; -using System.Text.RegularExpressions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public class NpgsqlTestStore : RelationalTestStore -{ - private readonly string? _scriptPath; - private readonly string? _additionalSql; - - private const string Northwind = "Northwind"; - - public const int CommandTimeout = 600; - - public static readonly string NorthwindConnectionString = CreateConnectionString(Northwind); - - public static async Task GetNorthwindStoreAsync() - => (NpgsqlTestStore)await NpgsqlNorthwindTestStoreFactory.Instance - .GetOrCreate(NpgsqlNorthwindTestStoreFactory.Name).InitializeAsync(null, (Func?)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); - - public static NpgsqlTestStore Create(string name, string? connectionStringOptions = null) - => new(name, connectionStringOptions: connectionStringOptions, shared: false); - - public static Task CreateInitializedAsync(string name) - => new NpgsqlTestStore(name, shared: false).InitializeNpgsqlAsync(null, (Func?)null, null); - - public NpgsqlTestStore( - string name, - string? scriptPath = null, - string? additionalSql = null, - string? connectionStringOptions = null, - bool shared = true) - : base(name, shared, CreateConnection(name, connectionStringOptions)) - { - Name = name; - - if (scriptPath is not null) - { - // ReSharper disable once AssignNullToNotNullAttribute - _scriptPath = Path.Combine(Path.GetDirectoryName(typeof(NpgsqlTestStore).GetTypeInfo().Assembly.Location)!, scriptPath); - } - - _additionalSql = additionalSql; - } - - private static NpgsqlConnection CreateConnection(string name, string? connectionStringOptions) - => new(CreateConnectionString(name, connectionStringOptions)); - - // ReSharper disable once MemberCanBePrivate.Global - public async Task InitializeNpgsqlAsync( - IServiceProvider? serviceProvider, - Func? createContext, - Func? seed) - => (NpgsqlTestStore)await InitializeAsync(serviceProvider, createContext, seed); - - // ReSharper disable once UnusedMember.Global - public async Task InitializeNpgsqlAsync( - IServiceProvider serviceProvider, - Func createContext, - Func seed) - => await InitializeNpgsqlAsync(serviceProvider, () => createContext(this), seed); - - protected override async Task InitializeAsync(Func createContext, Func? seed, Func? clean) - { - if (await CreateDatabaseAsync(clean)) - { - if (_scriptPath is not null) - { - ExecuteScript(_scriptPath); - - if (_additionalSql is not null) - { - Execute(Connection, command => command.ExecuteNonQuery(), _additionalSql); - } - } - else - { - await using var context = createContext(); - await context.Database.EnsureCreatedResilientlyAsync(); - - if (_additionalSql is not null) - { - Execute(Connection, command => command.ExecuteNonQuery(), _additionalSql); - } - - if (seed is not null) - { - await seed(context); - } - } - } - } - - public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) - { - 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(); - - return UseConnectionString - ? builder.UseNpgsql(ConnectionString, npgsqlOptionsBuilder) - : builder.UseNpgsql(Connection, npgsqlOptionsBuilder); - } - - private async Task CreateDatabaseAsync(Func? clean) - { - await using var master = new NpgsqlConnection(CreateAdminConnectionString()); - - if (await DatabaseExistsAsync(Name)) - { - if (_scriptPath is not null) - { - return false; - } - - 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 async Task WaitForExistsAsync(NpgsqlConnection connection) - { - var retryCount = 0; - while (true) - { - try - { - if (connection.State != ConnectionState.Closed) - { - await connection.CloseAsync(); - } - - NpgsqlConnection.ClearPool(connection); - - await connection.OpenAsync(); - await connection.CloseAsync(); - return; - } - catch (PostgresException e) - { - if (++retryCount >= 30 - || e.SqlState != "08001" && e.SqlState != "08000" && e.SqlState != "08006") - { - throw; - } - - await Task.Delay(100); - } - } - } - - // ReSharper disable once MemberCanBePrivate.Global - public void ExecuteScript(string scriptPath) - { - var script = File.ReadAllText(scriptPath); - Execute( - Connection, command => - { - foreach (var batch in - new Regex("^GO", RegexOptions.IgnoreCase | RegexOptions.Multiline, TimeSpan.FromMilliseconds(1000.0)) - .Split(script).Where(b => !string.IsNullOrEmpty(b))) - { - command.CommandText = batch; - command.ExecuteNonQuery(); - } - - return 0; - }, ""); - } - - private static string GetCreateDatabaseStatement(string name) - => $""" - CREATE DATABASE "{name}" - """; - - private static async Task DatabaseExistsAsync(string name) - { - await using var master = new NpgsqlConnection(CreateAdminConnectionString()); - - return await ExecuteScalarAsync(master, $@"SELECT COUNT(*) FROM pg_database WHERE datname = '{name}'") > 0; - } - - public async Task DeleteDatabaseAsync() - { - if (!await DatabaseExistsAsync(Name)) - { - return; - } - - await using var master = new NpgsqlConnection(CreateAdminConnectionString()); - - await ExecuteNonQueryAsync(master, GetDisconnectDatabaseSql(Name)); - await ExecuteNonQueryAsync(master, GetDropDatabaseSql(Name)); - - NpgsqlConnection.ClearAllPools(); - } - - // Kill all connection to the database - private static string GetDisconnectDatabaseSql(string name) - => $""" -REVOKE CONNECT ON DATABASE "{name}" FROM PUBLIC; -SELECT pg_terminate_backend (pg_stat_activity.pid) - FROM pg_stat_activity - WHERE datname = '{name}' -"""; - - private static string GetDropDatabaseSql(string name) - => $""" - DROP DATABASE "{name}" - """; - - public override void OpenConnection() - => Connection.Open(); - - public override Task OpenConnectionAsync() - => Connection.OpenAsync(); - - // ReSharper disable once UnusedMember.Global - 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); - - // 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); - - // 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) - => 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) - => 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) - => Execute( - connection, command => - { - using var dataReader = command.ExecuteReader(); - - 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) - => ExecuteAsync( - connection, async command => - { - await using var dataReader = await command.ExecuteReaderAsync(); - - var results = Enumerable.Empty(); - while (await dataReader.ReadAsync()) - { - results = results.Concat([await dataReader.GetFieldValueAsync(0)]); - } - - return results; - }, sql, false, parameters); - - private static T Execute( - DbConnection connection, - Func execute, - string sql, - bool useTransaction = false, - object[]? parameters = null) - => ExecuteCommand(connection, execute, sql, useTransaction, parameters); - - private static T ExecuteCommand( - DbConnection connection, - Func execute, - string sql, - bool useTransaction, - object[]? parameters) - { - if (connection.State != ConnectionState.Closed) - { - connection.Close(); - } - - connection.Open(); - try - { - using var transaction = useTransaction ? connection.BeginTransaction() : null; - - T result; - using (var command = CreateCommand(connection, sql, parameters)) - { - command.Transaction = transaction; - result = execute(command); - } - - transaction?.Commit(); - - return result; - } - finally - { - if (connection.State == ConnectionState.Closed - && connection.State != ConnectionState.Closed) - { - connection.Close(); - } - } - } - - private static Task ExecuteAsync( - DbConnection connection, - Func> executeAsync, - string sql, - bool useTransaction = false, - IReadOnlyList? parameters = null) - => ExecuteCommandAsync(connection, executeAsync, sql, useTransaction, parameters); - - private static async Task ExecuteCommandAsync( - DbConnection connection, - Func> executeAsync, - string sql, - bool useTransaction, - IReadOnlyList? parameters) - { - if (connection.State != ConnectionState.Closed) - { - await connection.CloseAsync(); - } - - await connection.OpenAsync(); - try - { - await using var transaction = useTransaction ? await connection.BeginTransactionAsync() : null; - - T result; - await using (var command = CreateCommand(connection, sql, parameters)) - { - result = await executeAsync(command); - } - - if (transaction is not null) - { - await transaction.CommitAsync(); - } - - return result; - } - finally - { - if (connection.State == ConnectionState.Closed - && connection.State != ConnectionState.Closed) - { - await connection.CloseAsync(); - } - } - } - - private static DbCommand CreateCommand( - DbConnection connection, - string commandText, - IReadOnlyList? parameters = null) - { - var command = (NpgsqlCommand)connection.CreateCommand(); - - command.CommandText = commandText; - command.CommandTimeout = CommandTimeout; - - if (parameters is not null) - { - for (var i = 0; i < parameters.Count; i++) - { - command.Parameters.AddWithValue("p" + i, parameters[i]); - } - } - - return command; - } - - public static string CreateConnectionString(string name, string? options = null) - { - var builder = new NpgsqlConnectionStringBuilder(TestEnvironment.DefaultConnection) { Database = name }; - - if (options is not null) - { - builder.Options = options; - } - - return builder.ConnectionString; - } - - private static string CreateAdminConnectionString() - => CreateConnectionString("postgres"); - - 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 deleted file mode 100644 index 06082bacff..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStoreFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public class NpgsqlTestStoreFactory( - string? scriptPath = null, - string? additionalSql = null, - string? connectionStringOptions = null, - bool useConnectionString = false) : RelationalTestStoreFactory -{ - public static NpgsqlTestStoreFactory Instance { get; } = new(); - - public override TestStore Create(string storeName) - => new NpgsqlTestStore(storeName, scriptPath, additionalSql, connectionStringOptions, shared: false) { UseConnectionString = useConnectionString }; - - public override TestStore GetOrCreate(string storeName) - => new NpgsqlTestStore(storeName, scriptPath, additionalSql, connectionStringOptions, shared: true) { UseConnectionString = useConnectionString }; - - public override IServiceCollection AddProviderServices(IServiceCollection serviceCollection) - => serviceCollection.AddEntityFrameworkNpgsql(); -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/RequiresPostgisAttribute.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/RequiresPostgisAttribute.cs deleted file mode 100644 index 17266f9b05..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/RequiresPostgisAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Globalization; - -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] -public sealed class RequiresPostgisAttribute : Attribute, ITestCondition -{ - public ValueTask IsMetAsync() - => new(TestEnvironment.IsPostgisAvailable || Environment.GetEnvironmentVariable("NPGSQL_TEST_POSTGIS")?.ToLower(CultureInfo.InvariantCulture) is "1" or "true"); - - public string SkipReason - => "PostGIS isn't installed, skipping"; -} diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs deleted file mode 100644 index 2d5e364d40..0000000000 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/TestNpgsqlConnection.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Data; -using System.Data.Common; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore.TestUtilities; - -public class TestNpgsqlConnection(RelationalConnectionDependencies dependencies, DbDataSource? dataSource = null) - : NpgsqlRelationalConnection(dependencies, dataSource) -{ - public string ErrorCode { get; set; } = "XX000"; - public Queue OpenFailures { get; } = new(); - public int OpenCount { get; set; } - public Queue CommitFailures { get; } = new(); - public Queue ExecutionFailures { get; } = new(); - public int ExecutionCount { get; set; } - - public override bool Open(bool errorsExpected = false) - { - PreOpen(); - - return base.Open(errorsExpected); - } - - public override Task OpenAsync(CancellationToken cancellationToken, bool errorsExpected = false) - { - PreOpen(); - - return base.OpenAsync(cancellationToken, errorsExpected); - } - - private void PreOpen() - { - if (DbConnection.State == ConnectionState.Open) - { - return; - } - - OpenCount++; - if (OpenFailures.Count <= 0) - { - return; - } - - var fail = OpenFailures.Dequeue(); - - if (fail.HasValue) - { - throw new PostgresException("Simulated failure", "ERROR", "ERROR", ErrorCode); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs deleted file mode 100644 index 0c69583c96..0000000000 --- a/test/EFCore.PG.FunctionalTests/TransactionInterceptionNpgsqlTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public abstract class TransactionInterceptionNpgsqlTestBase(TransactionInterceptionNpgsqlTestBase.InterceptionNpgsqlFixtureBase fixture) - : TransactionInterceptionTestBase(fixture) -{ - public abstract class InterceptionNpgsqlFixtureBase : InterceptionFixtureBase - { - protected override string StoreName - => "TransactionInterception"; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override IServiceCollection InjectInterceptors( - IServiceCollection serviceCollection, - IEnumerable injectedInterceptors) - => base.InjectInterceptors(serviceCollection.AddEntityFrameworkNpgsql(), injectedInterceptors); - } - - public class TransactionInterceptionNpgsqlTest(TransactionInterceptionNpgsqlTest.InterceptionNpgsqlFixture fixture) - : TransactionInterceptionNpgsqlTestBase(fixture), IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override bool ShouldSubscribeToDiagnosticListener - => false; - } - } - - public class TransactionInterceptionWithDiagnosticsNpgsqlTest( - TransactionInterceptionWithDiagnosticsNpgsqlTest.InterceptionNpgsqlFixture fixture) - : TransactionInterceptionNpgsqlTestBase(fixture), - IClassFixture - { - public class InterceptionNpgsqlFixture : InterceptionNpgsqlFixtureBase - { - protected override bool ShouldSubscribeToDiagnosticListener - => true; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs deleted file mode 100644 index 5fd149f03b..0000000000 --- a/test/EFCore.PG.FunctionalTests/TransactionNpgsqlTest.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore; - -public class TransactionNpgsqlTest(TransactionNpgsqlTest.TransactionNpgsqlFixture fixture) - : TransactionTestBase(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) - => Task.CompletedTask; - -#pragma warning disable CS0618 // AutoTransactionsEnabled is obsolete - public override Task SaveChanges_can_be_used_with_AutoTransactionsEnabled_false(bool async) - // Npgsql batches the inserts, creating an implicit transaction which fails the test - // (see https://github.com/npgsql/npgsql/issues/1307) - => Task.CompletedTask; -#pragma warning restore CS0618 - - protected override DbContext CreateContextWithConnectionString() - { - var options = Fixture.AddOptions( - new DbContextOptionsBuilder() - .UseNpgsql( - TestStore.ConnectionString, - b => b.ApplyConfiguration() - .ExecutionStrategy(c => new NpgsqlExecutionStrategy(c)) - .ReverseNullOrdering())) - .UseInternalServiceProvider(Fixture.ServiceProvider); - - return new DbContext(options.Options); - } - - // In PostgreSQL, once the transaction enters the failed state it is always rolled back completely, - // so none of the inserts are left. - public override async Task SaveChanges_can_be_used_with_no_savepoint(bool async) - { - await using (var context = CreateContext()) - { - context.Database.AutoSavepointsEnabled = false; - - await using var transaction = async - ? await context.Database.BeginTransactionAsync() - : context.Database.BeginTransaction(); - - context.Add(new TransactionCustomer { Id = 77, Name = "Bobble" }); - - if (async) - { - await context.SaveChangesAsync(); - } - else - { - context.SaveChanges(); - } - - context.Add(new TransactionCustomer { Id = 78, Name = "Hobble" }); - context.Add(new TransactionCustomer { Id = 1, Name = "Gobble" }); // Cause SaveChanges failure - - if (async) - { - await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - await transaction.CommitAsync(); - } - else - { - Assert.Throws(() => context.SaveChanges()); - transaction.Commit(); - } - - context.Database.AutoSavepointsEnabled = true; - } - - await using (var context = CreateContext()) - { - Assert.Equal(2, context.Set().Max(c => c.Id)); - } - } - - // Test generates an exception (by double-releasing the savepoint), which causes the transaction to enter - // a failed state and roll back all changes. - public override Task Savepoint_can_be_released(bool async) - => Task.CompletedTask; - - protected override bool AmbientTransactionsSupported - => true; - - protected override bool SnapshotSupported - => true; - - protected override bool DirtyReadsOccur - => false; - - public class TransactionNpgsqlFixture : TransactionFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new NpgsqlDbContextOptionsBuilder( - base.AddOptions(builder)) - .ExecutionStrategy(c => new NpgsqlExecutionStrategy(c)); - return builder; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs deleted file mode 100644 index 3146478e62..0000000000 --- a/test/EFCore.PG.FunctionalTests/TwoDatabasesNpgsqlTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class TwoDatabasesNpgsqlTest(NpgsqlFixture fixture) : TwoDatabasesTestBase(fixture), IClassFixture -{ - protected new NpgsqlFixture Fixture - => (NpgsqlFixture)base.Fixture; - - protected override DbContextOptionsBuilder CreateTestOptions( - DbContextOptionsBuilder optionsBuilder, - bool withConnectionString = false, - bool withNullConnectionString = false) - => withConnectionString - ? withNullConnectionString - ? optionsBuilder.UseNpgsql((string?)null) - : optionsBuilder.UseNpgsql(DummyConnectionString) - : optionsBuilder.UseNpgsql(); - - protected override TwoDatabasesWithDataContext CreateBackingContext(string databaseName) - => new(Fixture.CreateOptions(NpgsqlTestStore.Create(databaseName))); - - protected override string DummyConnectionString { get; } = "Host=localhost;Database=DoesNotExist"; -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlBoolTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlBoolTypeTest.cs deleted file mode 100644 index de95a3610f..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlBoolTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Miscellaneous; - -public class NpgsqlBoolTypeTest(NpgsqlBoolTypeTest.BoolTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='True' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='True' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS boolean)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":false,"Value":false}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='False' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(FALSE::boolean)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class BoolTypeFixture : NpgsqlTypeFixture - { - public override bool Value { get; } = true; - public override bool OtherValue { get; } = false; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlByteArrayTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlByteArrayTypeTest.cs deleted file mode 100644 index e48ee770da..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlByteArrayTypeTest.cs +++ /dev/null @@ -1,111 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Miscellaneous; - -public class ByteArrayTypeTest(ByteArrayTypeTest.ByteArrayTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='0x010203' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='0x010203' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (decode(j."JsonContainer" ->> 'Value', 'base64')) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"BAUGBw==","Value":"BAUGBw=="}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='0x04050607' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(encode(@Fixture_OtherValue, 'base64'))) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(encode(BYTEA E'\\x04050607', 'base64'))) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(encode(decode(j."JsonContainer" ->> 'OtherValue', 'base64'), 'base64'))) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(encode(j."OtherValue", 'base64'))) -"""); - } - - #endregion JSON - - public class ByteArrayTypeFixture() : NpgsqlTypeFixture - { - public override byte[] Value { get; } = [1, 2, 3]; - public override byte[] OtherValue { get; } = [4, 5, 6, 7]; - - public override Func Comparer { get; } = (a, b) => a.SequenceEqual(b); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlGuidTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlGuidTypeTest.cs deleted file mode 100644 index 057f204ee1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlGuidTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Miscellaneous; - -public class NpgsqlGuidTypeTest(NpgsqlGuidTypeTest.GuidTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='8f7331d6-cde9-44fb-8611-81fff686f280' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='8f7331d6-cde9-44fb-8611-81fff686f280' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS uuid)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"ae192c36-9004-49b2-b785-8be10d169627","Value":"ae192c36-9004-49b2-b785-8be10d169627"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='ae192c36-9004-49b2-b785-8be10d169627' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb('ae192c36-9004-49b2-b785-8be10d169627'::uuid)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class GuidTypeFixture : NpgsqlTypeFixture - { - public override Guid Value { get; } = new("8f7331d6-cde9-44fb-8611-81fff686f280"); - public override Guid OtherValue { get; } = new("ae192c36-9004-49b2-b785-8be10d169627"); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlStringTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlStringTypeTest.cs deleted file mode 100644 index fca9ec3802..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Miscellaneous/NpgsqlStringTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Miscellaneous; - -public class StringTypeTest(StringTypeTest.StringTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='foo' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='foo' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (j."JsonContainer" ->> 'Value') = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"bar","Value":"bar"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='bar' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb('bar'::text)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class StringTypeFixture : NpgsqlTypeFixture - { - public override string Value { get; } = "foo"; - public override string OtherValue { get; } = "bar"; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Networking/NpgsqlInetTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Networking/NpgsqlInetTypeTest.cs deleted file mode 100644 index cdedfcb3d4..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Networking/NpgsqlInetTypeTest.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Net; - -namespace Microsoft.EntityFrameworkCore.Types.Networking; - -public class NpgsqlInetTypeTest(NpgsqlInetTypeTest.InetTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='192.168.1.1' (DbType = Object) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='192.168.1.1' (DbType = Object) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS inet)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"192.168.1.2","Value":"192.168.1.2"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='192.168.1.2' (DbType = Object) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(INET '192.168.1.2'::inet)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class InetTypeFixture : NpgsqlTypeFixture - { - public override IPAddress Value { get; } = IPAddress.Parse("192.168.1.1"); - public override IPAddress OtherValue { get; } = IPAddress.Parse("192.168.1.2"); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Networking/NpgsqlMacaddrTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Networking/NpgsqlMacaddrTypeTest.cs deleted file mode 100644 index 3c1ade7334..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Networking/NpgsqlMacaddrTypeTest.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Net.NetworkInformation; - -namespace Microsoft.EntityFrameworkCore.Types.Networking; - -public class NpgsqlMacaddrTypeTest(NpgsqlMacaddrTypeTest.MacaddrTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='001422012345' (DbType = Object) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='001422012345' (DbType = Object) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS macaddr)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"001422012346","Value":"001422012346"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='001422012346' (DbType = Object) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(MACADDR '001422012346'::macaddr)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class MacaddrTypeFixture : NpgsqlTypeFixture - { - public override PhysicalAddress Value { get; } = PhysicalAddress.Parse("00-14-22-01-23-45"); - public override PhysicalAddress OtherValue { get; } = PhysicalAddress.Parse("00-14-22-01-23-46"); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/NpgsqlTypeFixture.cs b/test/EFCore.PG.FunctionalTests/Types/NpgsqlTypeFixture.cs deleted file mode 100644 index ba01ce071a..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/NpgsqlTypeFixture.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types; - -public abstract class NpgsqlTypeFixture : RelationalTypeFixtureBase -{ - protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlDecimalTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlDecimalTypeTest.cs deleted file mode 100644 index 641d42d7b1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlDecimalTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Numeric; - -public class NpgsqlDecimalTypeTest(NpgsqlDecimalTypeTest.DecimalTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='30.5' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='30.5' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS numeric)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":30,"Value":30}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='30' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(30.0::numeric)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class DecimalTypeFixture : NpgsqlTypeFixture - { - public override decimal Value { get; } = 30.5m; - public override decimal OtherValue { get; } = 30m; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlDoubleTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlDoubleTypeTest.cs deleted file mode 100644 index 3c9a2acb11..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlDoubleTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Numeric; - -public class NpgsqlDoubleTypeTest(NpgsqlDoubleTypeTest.DoubleTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='30.5' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='30.5' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS double precision)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":30,"Value":30}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='30' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(30.0::double precision)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class DoubleTypeFixture : NpgsqlTypeFixture - { - public override double Value { get; } = 30.5d; - public override double OtherValue { get; } = 30d; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlFloatTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlFloatTypeTest.cs deleted file mode 100644 index d5504e90e6..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlFloatTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Numeric; - -public class NpgsqlFloatTypeTest(NpgsqlFloatTypeTest.FloatTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='30.5' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='30.5' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS real)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":30,"Value":30}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='30' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(30::real)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class FloatTypeFixture : NpgsqlTypeFixture - { - public override float Value { get; } = 30.5f; - public override float OtherValue { get; } = 30f; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlIntTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlIntTypeTest.cs deleted file mode 100644 index 482b86bc4e..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlIntTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Numeric; - -public class NpgsqlIntTypeTest(NpgsqlIntTypeTest.IntTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='-2147483648' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='-2147483648' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS integer)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":2147483647,"Value":2147483647}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='2147483647' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(2147483647::int)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class IntTypeFixture : NpgsqlTypeFixture - { - public override int Value { get; } = int.MinValue; - public override int OtherValue { get; } = int.MaxValue; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlLongTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlLongTypeTest.cs deleted file mode 100644 index 921c4635f4..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlLongTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Numeric; - -public class NpgsqlLongTypeTest(NpgsqlLongTypeTest.LongTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='-9223372036854775808' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='-9223372036854775808' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS bigint)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":9223372036854775807,"Value":9223372036854775807}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='9223372036854775807' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(9223372036854775807::bigint)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class LongTypeFixture : NpgsqlTypeFixture - { - public override long Value { get; } = long.MinValue; - public override long OtherValue { get; } = long.MaxValue; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlShortTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlShortTypeTest.cs deleted file mode 100644 index e7c2f56cef..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Numeric/NpgsqlShortTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Numeric; - -public class NpgsqlShortTypeTest(NpgsqlShortTypeTest.ShortTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='-32768' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='-32768' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS smallint)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":32767,"Value":32767}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='32767' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(32767::smallint)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class ShortTypeFixture : NpgsqlTypeFixture - { - public override short Value { get; } = short.MinValue; - public override short OtherValue { get; } = short.MaxValue; - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateOnlyTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateOnlyTypeTest.cs deleted file mode 100644 index df22d978d5..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateOnlyTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Temporal; - -public class NpgsqlBoolTypeTest(NpgsqlBoolTypeTest.DateOnlyTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='01/05/2020' (DbType = Date) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='01/05/2020' (DbType = Date) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS date)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"2022-05-03","Value":"2022-05-03"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='05/03/2022' (DbType = Date) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(DATE '2022-05-03'::date)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class DateOnlyTypeFixture : NpgsqlTypeFixture - { - public override DateOnly Value { get; } = new DateOnly(2020, 1, 5); - public override DateOnly OtherValue { get; } = new DateOnly(2022, 5, 3); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeOffsetTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeOffsetTypeTest.cs deleted file mode 100644 index 95ba4cc728..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeOffsetTypeTest.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Temporal; - -public class DateTimeOffsetTypeTest(DateTimeOffsetTypeTest.DateTimeOffsetTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='2020-01-05T12:30:45.0000000+00:00' (DbType = DateTime) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='2020-01-05T12:30:45.0000000+00:00' (DbType = DateTime) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS timestamp with time zone)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"2020-01-05T13:30:45\u002B00:00","Value":"2020-01-05T13:30:45\u002B00:00"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='2020-01-05T13:30:45.0000000+00:00' (DbType = DateTime) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(TIMESTAMPTZ '2020-01-05T13:30:45+00:00'::timestamptz)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class DateTimeOffsetTypeFixture : NpgsqlTypeFixture - { - // Note that we don't support DateTimeOffset with Offset != 0, since the offset doesn't get stored in the database. - public override DateTimeOffset Value { get; } = new DateTimeOffset(2020, 1, 5, 12, 30, 45, TimeSpan.Zero); - public override DateTimeOffset OtherValue { get; } = new DateTimeOffset(2020, 1, 5, 13, 30, 45, TimeSpan.Zero); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeUnspecifiedTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeUnspecifiedTypeTest.cs deleted file mode 100644 index 57bc0c4ec0..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeUnspecifiedTypeTest.cs +++ /dev/null @@ -1,111 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Temporal; - -public class DateTimeUnspecifiedTypeTest(DateTimeUnspecifiedTypeTest.DateTimeTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='2020-01-05T12:30:45.0000000' - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='2020-01-05T12:30:45.0000000' - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS timestamp without time zone)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"2022-05-03T00:00:00","Value":"2022-05-03T00:00:00"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='2022-05-03T00:00:00.0000000' - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(TIMESTAMP '2022-05-03T00:00:00'::timestamp)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class DateTimeTypeFixture : NpgsqlTypeFixture - { - public override string? StoreType => "timestamp without time zone"; - - public override DateTime Value { get; } = new DateTime(2020, 1, 5, 12, 30, 45, DateTimeKind.Unspecified); - public override DateTime OtherValue { get; } = new DateTime(2022, 5, 3, 0, 0, 0, DateTimeKind.Unspecified); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeUtcTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeUtcTypeTest.cs deleted file mode 100644 index 8f26b8942f..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlDateTimeUtcTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Temporal; - -public class DateTimeUtcTypeTest(DateTimeUtcTypeTest.DateTimeTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='2020-01-05T12:30:45.0000000Z' (DbType = DateTime) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='2020-01-05T12:30:45.0000000Z' (DbType = DateTime) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS timestamp with time zone)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"2022-05-03T00:00:00Z","Value":"2022-05-03T00:00:00Z"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='2022-05-03T00:00:00.0000000Z' (DbType = DateTime) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(TIMESTAMPTZ '2022-05-03T00:00:00Z'::timestamptz)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class DateTimeTypeFixture : NpgsqlTypeFixture - { - public override DateTime Value { get; } = new DateTime(2020, 1, 5, 12, 30, 45, DateTimeKind.Utc); - public override DateTime OtherValue { get; } = new DateTime(2022, 5, 3, 0, 0, 0, DateTimeKind.Utc); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlTimeOnlyTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlTimeOnlyTypeTest.cs deleted file mode 100644 index e3c978f922..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlTimeOnlyTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Temporal; - -public class NpgsqlTimeOnlyTypeTest(NpgsqlTimeOnlyTypeTest.TimeOnlyTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='12:30' (DbType = Time) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='12:30' (DbType = Time) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS time without time zone)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"14:00:00.0000000","Value":"14:00:00.0000000"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='14:00' (DbType = Time) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(TIME '14:00:00'::time without time zone)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class TimeOnlyTypeFixture : NpgsqlTypeFixture - { - public override TimeOnly Value { get; } = new TimeOnly(12, 30, 45); - public override TimeOnly OtherValue { get; } = new TimeOnly(14, 0, 0); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlTimeSpanTypeTest.cs b/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlTimeSpanTypeTest.cs deleted file mode 100644 index 3093cb533c..0000000000 --- a/test/EFCore.PG.FunctionalTests/Types/Temporal/NpgsqlTimeSpanTypeTest.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Types.Temporal; - -public class NpgsqlTimeSpanTypeTest(NpgsqlTimeSpanTypeTest.TimeSpanTypeFixture fixture, ITestOutputHelper testOutputHelper) - : RelationalTypeTestBase(fixture, testOutputHelper) -{ - public override async Task Equality_in_query() - { - await base.Equality_in_query(); - - AssertSql( - """ -@Fixture_Value='12:30:45' (DbType = Object) - -SELECT t."Id", t."OtherValue", t."Value" -FROM "TypeEntity" AS t -WHERE t."Value" = @Fixture_Value -LIMIT 2 -"""); - } - - #region JSON - - public override async Task Query_property_within_json() - { - await base.Query_property_within_json(); - - AssertSql( - """ -@Fixture_Value='12:30:45' (DbType = Object) - -SELECT j."Id", j."OtherValue", j."Value", j."JsonContainer" -FROM "JsonTypeEntity" AS j -WHERE (CAST(j."JsonContainer" ->> 'Value' AS interval)) = @Fixture_Value -LIMIT 2 -"""); - } - - public override async Task SaveChanges_within_json() - { - await base.SaveChanges_within_json(); - - AssertSql( - """ -@p0='{"OtherValue":"14:00:00","Value":"14:00:00"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonTypeEntity" SET "JsonContainer" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task ExecuteUpdate_within_json_to_parameter() - { - await base.ExecuteUpdate_within_json_to_parameter(); - - AssertSql( - """ -@Fixture_OtherValue='14:00:00' (DbType = Object) - -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(@Fixture_OtherValue)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_constant() - { - await base.ExecuteUpdate_within_json_to_constant(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(INTERVAL '14:00:00'::interval)) -"""); - } - - public override async Task ExecuteUpdate_within_json_to_another_json_property() - { - await base.ExecuteUpdate_within_json_to_another_json_property(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', j."JsonContainer" -> 'OtherValue') -"""); - } - - public override async Task ExecuteUpdate_within_json_to_nonjson_column() - { - await base.ExecuteUpdate_within_json_to_nonjson_column(); - - AssertSql( - """ -UPDATE "JsonTypeEntity" AS j -SET "JsonContainer" = jsonb_set(j."JsonContainer", '{Value}', to_jsonb(j."OtherValue")) -"""); - } - - #endregion JSON - - public class TimeSpanTypeFixture : NpgsqlTypeFixture - { - public override TimeSpan Value { get; } = new TimeSpan(12, 30, 45); - public override TimeSpan OtherValue { get; } = new TimeSpan(14, 0, 0); - } - - [ConditionalFact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.PG.FunctionalTests/Update/ComplexCollectionJsonUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/ComplexCollectionJsonUpdateNpgsqlTest.cs deleted file mode 100644 index 825622b1a8..0000000000 --- a/test/EFCore.PG.FunctionalTests/Update/ComplexCollectionJsonUpdateNpgsqlTest.cs +++ /dev/null @@ -1,276 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Update; - -public class ComplexCollectionJsonUpdateNpgsqlTest : ComplexCollectionJsonUpdateTestBase< - ComplexCollectionJsonUpdateNpgsqlTest.ComplexCollectionJsonUpdateNpgsqlFixture> -{ - public ComplexCollectionJsonUpdateNpgsqlTest(ComplexCollectionJsonUpdateNpgsqlFixture fixture) - : base(fixture) - => ClearLog(); - - public override async Task Add_element_to_complex_collection_mapped_to_json() - { - await base.Add_element_to_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]},{"Name":"New Contact","PhoneNumbers":["555-0000"]}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Remove_element_from_complex_collection_mapped_to_json() - { - await base.Remove_element_from_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Modify_element_in_complex_collection_mapped_to_json() - { - await base.Modify_element_in_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"First Contact - Modified","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Move_elements_in_complex_collection_mapped_to_json() - { - await base.Move_elements_in_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]},{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Change_complex_collection_mapped_to_json_to_null_and_to_empty() - { - await base.Change_complex_collection_mapped_to_json_to_null_and_to_empty(); - - AssertSql( - """ -@p0='[]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -""", - // - """ -@p0=NULL (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Complex_collection_with_nested_complex_type_mapped_to_json() - { - await base.Complex_collection_with_nested_complex_type_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"John Doe","PhoneNumbers":["555-1234","555-5678"],"Address":{"City":"Seattle","Country":"USA","PostalCode":"98101","Street":"123 Main St"}},{"Name":"Jane Smith","PhoneNumbers":["555-9876"],"Address":{"City":"Portland","Country":"USA","PostalCode":"97201","Street":"456 Oak Ave"}}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Modify_multiple_complex_properties_mapped_to_json() - { - await base.Modify_multiple_complex_properties_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Contact 1","PhoneNumbers":["555-1111"]}]' (Nullable = false) (DbType = Object) -@p1='{"Budget":50000.00,"Name":"Department A"}' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "Companies" SET "Contacts" = @p0, "Department" = @p1 -WHERE "Id" = @p2; -"""); - } - - public override async Task Clear_complex_collection_mapped_to_json() - { - await base.Clear_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Replace_entire_complex_collection_mapped_to_json() - { - await base.Replace_entire_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Replacement Contact 1","PhoneNumbers":["999-1111"]},{"Name":"Replacement Contact 2","PhoneNumbers":["999-2222","999-3333"]}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Contacts" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Add_element_to_nested_complex_collection_mapped_to_json() - { - await base.Add_element_to_nested_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Initial Employee","PhoneNumbers":["555-0001","555-9999"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Modify_nested_complex_property_in_complex_collection_mapped_to_json() - { - await base.Modify_nested_complex_property_in_complex_collection_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Modified City","Country":"USA","PostalCode":"99999","Street":"100 First St"}}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Set_complex_collection_to_null_mapped_to_json() - { - await base.Set_complex_collection_to_null_mapped_to_json(); - - AssertSql( - """ -@p0=NULL (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Set_null_complex_collection_to_non_empty_mapped_to_json() - { - await base.Set_null_complex_collection_to_non_empty_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"New Employee","PhoneNumbers":["555-1111"],"Address":{"City":"New City","Country":"USA","PostalCode":"12345","Street":"123 New St"}}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Replace_complex_collection_element_mapped_to_json() - { - await base.Replace_complex_collection_element_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Replacement Employee","PhoneNumbers":["555-7777","555-8888"],"Address":{"City":"Replace City","Country":"Canada","PostalCode":"54321","Street":"789 Replace St"}}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Complex_collection_with_empty_nested_collections_mapped_to_json() - { - await base.Complex_collection_with_empty_nested_collections_mapped_to_json(); - - AssertSql( - """ -@p0='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}},{"Name":"Employee No Phone","PhoneNumbers":[],"Address":{"City":"Quiet City","Country":"USA","PostalCode":"00000","Street":"456 No Phone St"}}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Employees" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Set_complex_property_mapped_to_json_to_null() - { - await base.Set_complex_property_mapped_to_json_to_null(); - - AssertSql( - """ -@p0=NULL (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Department" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Set_null_complex_property_to_non_null_mapped_to_json() - { - await base.Set_null_complex_property_to_non_null_mapped_to_json(); - - AssertSql( - """ -@p0='{"Budget":25000.00,"Name":"New Department"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Department" = @p0 -WHERE "Id" = @p1; -"""); - } - - public override async Task Replace_complex_property_mapped_to_json() - { - await base.Replace_complex_property_mapped_to_json(); - - AssertSql( - """ -@p0='{"Budget":99999.99,"Name":"Replacement Department"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "Companies" SET "Department" = @p0 -WHERE "Id" = @p1; -"""); - } - - public class ComplexCollectionJsonUpdateNpgsqlFixture : ComplexCollectionJsonUpdateFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs deleted file mode 100644 index eeb8abaa76..0000000000 --- a/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs +++ /dev/null @@ -1,2146 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Update; - -public class JsonUpdateNpgsqlTest : JsonUpdateTestBase -{ - public JsonUpdateNpgsqlTest(JsonUpdateNpgsqlFixture fixture) - : base(fixture) - { - ClearLog(); - } - - public override async Task Add_element_to_json_collection_branch() - { - await base.Add_element_to_json_collection_branch(); - - AssertSql( - """ -@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) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Add_element_to_json_collection_leaf() - { - await base.Add_element_to_json_collection_leaf(); - - AssertSql( - """ -@p0='[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"},{"SomethingSomething":"ss1"}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch,OwnedCollectionLeaf}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Add_element_to_json_collection_on_derived() - { - await base.Add_element_to_json_collection_on_derived(); - - AssertSql( - """ -@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 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Discriminator", j."Name", j."Fraction", j."CollectionOnBase", j."ReferenceOnBase", j."CollectionOnDerived", j."ReferenceOnDerived" -FROM "JsonEntitiesInheritance" AS j -WHERE j."Discriminator" = 'JsonEntityInheritanceDerived' -LIMIT 2 -"""); - } - - public override async Task Add_element_to_json_collection_root() - { - await base.Add_element_to_json_collection_root(); - - AssertSql( - """ -@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 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Add_element_to_json_collection_root_null_navigations() - { - await base.Add_element_to_json_collection_root_null_navigations(); - - AssertSql( - """ -@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 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Add_entity_with_json() - { - await base.Add_entity_with_json(); - - AssertSql( - """ -@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) -@p4='NewEntity' - -INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "OwnedCollectionRoot", "Id", "EntityBasicId", "Name") -VALUES (@p0, @p1, @p2, @p3, @p4); -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Add_entity_with_json_null_navigations() - { - await base.Add_entity_with_json_null_navigations(); - - AssertSql( - """ -@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' - -INSERT INTO "JsonEntitiesBasic" ("OwnedReferenceRoot", "Id", "EntityBasicId", "Name") -VALUES (@p0, @p1, @p2, @p3); -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Add_json_reference_leaf() - { - await base.Add_json_reference_leaf(); - - AssertSql( - """ -@p0='{"SomethingSomething":"ss3"}' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch,0,OwnedReferenceLeaf}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Add_json_reference_root() - { - await base.Add_json_reference_root(); - - AssertSql( - """ -@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 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Delete_entity_with_json() - { - await base.Delete_entity_with_json(); - - AssertSql( - """ -@p0='1' - -DELETE FROM "JsonEntitiesBasic" -WHERE "Id" = @p0; -""", - // - """ -SELECT count(*)::int -FROM "JsonEntitiesBasic" AS j -"""); - } - - public override async Task Delete_json_collection_branch() - { - await base.Delete_json_collection_branch(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Delete_json_collection_root() - { - await base.Delete_json_collection_root(); - - AssertSql( - """ -@p0=NULL (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = @p0 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Delete_json_reference_leaf() - { - await base.Delete_json_reference_leaf(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedReferenceBranch,OwnedReferenceLeaf}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Delete_json_reference_root() - { - await base.Delete_json_reference_root(); - - AssertSql( - """ -@p0=NULL (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = @p0 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_element_in_json_collection_branch() - { - await base.Edit_element_in_json_collection_branch(); - - AssertSql( - """ -@p0='"2111-11-11T00:00:00"' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{0,OwnedCollectionBranch,0,Date}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_element_in_json_collection_root1() - { - await base.Edit_element_in_json_collection_root1(); - - AssertSql( - """ -@p0='"Modified"' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{0,Name}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_element_in_json_collection_root2() - { - await base.Edit_element_in_json_collection_root2(); - - AssertSql( - """ -@p0='"Modified"' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,Name}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_element_in_json_multiple_levels_partial_update() - { - await base.Edit_element_in_json_multiple_levels_partial_update(); - - AssertSql( - """ -@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 -WHERE "Id" = @p2; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection() - { - await base.Edit_element_in_json_branch_collection_and_add_element_to_the_same_collection(); - - AssertSql( - """ -@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) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_two_elements_in_the_same_json_collection() - { - await base.Edit_two_elements_in_the_same_json_collection(); - - AssertSql( - """ -@p0='[{"SomethingSomething":"edit1"},{"SomethingSomething":"edit2"}]' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{OwnedCollectionBranch,0,OwnedCollectionLeaf}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_two_elements_in_the_same_json_collection_at_the_root() - { - await base.Edit_two_elements_in_the_same_json_collection_at_the_root(); - - AssertSql( - """ -@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 -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_collection_element_and_reference_at_once() - { - await base.Edit_collection_element_and_reference_at_once(); - - AssertSql( - """ -@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) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_single_enum_property() - { - await base.Edit_single_enum_property(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_single_numeric_property() - { - await base.Edit_single_numeric_property(); - - AssertSql( - """ -@p0='1024' (Nullable = false) (DbType = Object) -@p1='999' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,Number}', @p0), "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{Number}', @p1) -WHERE "Id" = @p2; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_bool() - { - await base.Edit_single_property_bool(); - - AssertSql( - """ -@p0='true' (Nullable = false) (DbType = Object) -@p1='false' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestBoolean}', @p0), "Reference" = jsonb_set("Reference", '{TestBoolean}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_byte() - { - await base.Edit_single_property_byte(); - - AssertSql( - """ -@p0='14' (Nullable = false) (DbType = Object) -@p1='25' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestByte}', @p0), "Reference" = jsonb_set("Reference", '{TestByte}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_char() - { - await base.Edit_single_property_char(); - - AssertSql( - """ -@p0='"t"' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesAllTypes" SET "Reference" = jsonb_set("Reference", '{TestCharacter}', @p0) -WHERE "Id" = @p1; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_datetime() - { - await base.Edit_single_property_datetime(); - - AssertSql( - """ -@p0='"3000-01-01T12:34:56"' (Nullable = false) (DbType = Object) -@p1='"3000-01-01T12:34:56"' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTime}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTime}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_datetimeoffset() - { - await base.Edit_single_property_datetimeoffset(); - - AssertSql( - """ -@p0='"3000-01-01T12:34:56-04:00"' (Nullable = false) (DbType = Object) -@p1='"3000-01-01T12:34:56-04:00"' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTimeOffset}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTimeOffset}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_decimal() - { - await base.Edit_single_property_decimal(); - - AssertSql( - """ -@p0='-13579.01' (Nullable = false) (DbType = Object) -@p1='-13579.01' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDecimal}', @p0), "Reference" = jsonb_set("Reference", '{TestDecimal}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_double() - { - await base.Edit_single_property_double(); - - AssertSql( - """ -@p0='-1.23579' (Nullable = false) (DbType = Object) -@p1='-1.23579' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDouble}', @p0), "Reference" = jsonb_set("Reference", '{TestDouble}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_guid() - { - await base.Edit_single_property_guid(); - - AssertSql( - """ -@p0='"12345678-1234-4321-5555-987654321000"' (Nullable = false) (DbType = Object) -@p1='"12345678-1234-4321-5555-987654321000"' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestGuid}', @p0), "Reference" = jsonb_set("Reference", '{TestGuid}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_int16() - { - await base.Edit_single_property_int16(); - - AssertSql( - """ -@p0='-3234' (Nullable = false) (DbType = Object) -@p1='-3234' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt16}', @p0), "Reference" = jsonb_set("Reference", '{TestInt16}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_int32() - { - await base.Edit_single_property_int32(); - - AssertSql( - """ -@p0='-3234' (Nullable = false) (DbType = Object) -@p1='-3234' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestInt32}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_int64() - { - await base.Edit_single_property_int64(); - - AssertSql( - """ -@p0='-3234' (Nullable = false) (DbType = Object) -@p1='-3234' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt64}', @p0), "Reference" = jsonb_set("Reference", '{TestInt64}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_signed_byte() - { - await base.Edit_single_property_signed_byte(); - - AssertSql( - """ -@p0='-108' (Nullable = false) (DbType = Object) -@p1='-108' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSignedByte}', @p0), "Reference" = jsonb_set("Reference", '{TestSignedByte}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_single() - { - await base.Edit_single_property_single(); - - AssertSql( - """ -@p0='-7.234' (Nullable = false) (DbType = Object) -@p1='-7.234' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSingle}', @p0), "Reference" = jsonb_set("Reference", '{TestSingle}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_timespan() - { - await base.Edit_single_property_timespan(); - - AssertSql( - """ -@p0='"10:01:01.007"' (Nullable = false) (DbType = Object) -@p1='"10:01:01.007"' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeSpan}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeSpan}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_uint16() - { - await base.Edit_single_property_uint16(); - - AssertSql( - """ -@p0='1534' (Nullable = false) (DbType = Object) -@p1='1534' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt16}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt16}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_uint32() - { - await base.Edit_single_property_uint32(); - - AssertSql( - """ -@p0='1237775789' (Nullable = false) (DbType = Object) -@p1='1237775789' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt32}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_uint64() - { - await base.Edit_single_property_uint64(); - - AssertSql( - """ -@p0='1234555555123456789' (Nullable = false) (DbType = Object) -@p1='1234555555123456789' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt64}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt64}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_int32() - { - await base.Edit_single_property_nullable_int32(); - - AssertSql( - """ -@p0='122354' (Nullable = false) (DbType = Object) -@p1='64528' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_int32_set_to_null() - { - await base.Edit_single_property_nullable_int32_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_enum() - { - await base.Edit_single_property_enum(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_enum_with_int_converter() - { - await base.Edit_single_property_enum_with_int_converter(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_enum() - { - await base.Edit_single_property_nullable_enum(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_enum_set_to_null() - { - await base.Edit_single_property_nullable_enum_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnum}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnum}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_enum_with_int_converter() - { - await base.Edit_single_property_nullable_enum_with_int_converter(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_enum_with_int_converter_set_to_null() - { - await base.Edit_single_property_nullable_enum_with_int_converter_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithIntConverter}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithIntConverter}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_enum_with_converter_that_handles_nulls() - { - await base.Edit_single_property_nullable_enum_with_converter_that_handles_nulls(); - - AssertSql( - """ -@p0='"Three"' (Nullable = false) (DbType = Object) -@p1='"One"' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithConverterThatHandlesNulls}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithConverterThatHandlesNulls}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_nullable_enum_with_converter_that_handles_nulls_set_to_null() - { - await base.Edit_single_property_nullable_enum_with_converter_that_handles_nulls_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithConverterThatHandlesNulls}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithConverterThatHandlesNulls}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_two_properties_on_same_entity_updates_the_entire_entity() - { - await base.Edit_two_properties_on_same_entity_updates_the_entire_entity(); - - AssertSql( - """ -@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,9223372036854775807]}' (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,9223372036854775807]}' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0}', @p0), "Reference" = @p1 -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_a_scalar_property_and_reference_navigation_on_the_same_entity() - { - await base.Edit_a_scalar_property_and_reference_navigation_on_the_same_entity(); - - AssertSql( - """ -@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) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_a_scalar_property_and_collection_navigation_on_the_same_entity() - { - await base.Edit_a_scalar_property_and_collection_navigation_on_the_same_entity(); - - AssertSql( - """ -@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) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_a_scalar_property_and_another_property_behind_reference_navigation_on_the_same_entity() - { - await base.Edit_a_scalar_property_and_another_property_behind_reference_navigation_on_the_same_entity(); - - AssertSql( - """ -@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) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_with_converter_bool_to_int_zero_one() - { - await base.Edit_single_property_with_converter_bool_to_int_zero_one(); - - AssertSql( - """ -@p0='0' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{BoolConvertedToIntZeroOne}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_with_converter_bool_to_string_True_False() - { - await base.Edit_single_property_with_converter_bool_to_string_True_False(); - - AssertSql( - """ -@p0='"True"' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{BoolConvertedToStringTrueFalse}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_with_converter_bool_to_string_Y_N() - { - await base.Edit_single_property_with_converter_bool_to_string_Y_N(); - - AssertSql( - """ -@p0='"N"' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{BoolConvertedToStringYN}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_with_converter_int_zero_one_to_bool() - { - await base.Edit_single_property_with_converter_int_zero_one_to_bool(); - - AssertSql( - """ -@p0='true' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{IntZeroOneConvertedToBool}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - [ConditionalFact] - public override async Task Edit_single_property_with_converter_string_True_False_to_bool() - { - await base.Edit_single_property_with_converter_string_True_False_to_bool(); - - AssertSql( - """ -@p0='false' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{StringTrueFalseConvertedToBool}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - [ConditionalFact] - public override async Task Edit_single_property_with_converter_string_Y_N_to_bool() - { - await base.Edit_single_property_with_converter_string_Y_N_to_bool(); - - AssertSql( - """ -@p0='true' (Nullable = false) (DbType = Object) -@p1='1' - -UPDATE "JsonEntitiesConverters" SET "Reference" = jsonb_set("Reference", '{StringYNConvertedToBool}', @p0) -WHERE "Id" = @p1; -""", - // - """ -SELECT j."Id", j."Reference" -FROM "JsonEntitiesConverters" AS j -WHERE j."Id" = 1 -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_collection_of_numeric() - { - await base.Edit_single_property_collection_of_numeric(); - - AssertSql( - """ -@p0='[1024,2048]' (Nullable = false) (DbType = Object) -@p1='[999,997]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesBasic" SET "OwnedCollectionRoot" = jsonb_set("OwnedCollectionRoot", '{1,Numbers}', @p0), "OwnedReferenceRoot" = jsonb_set("OwnedReferenceRoot", '{Numbers}', @p1) -WHERE "Id" = @p2; -""", - // - """ -SELECT j."Id", j."EntityBasicId", j."Name", j."OwnedCollectionRoot", j."OwnedReferenceRoot" -FROM "JsonEntitiesBasic" AS j -LIMIT 2 -"""); - } - - public override async Task Edit_single_property_collection_of_bool() - { - await base.Edit_single_property_collection_of_bool(); - - AssertSql( - """ -@p0='[true,true,true,false]' (Nullable = false) (DbType = Object) -@p1='[true,true,false]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestBooleanCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestBooleanCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_byte() - { - await base.Edit_single_property_collection_of_byte(); - - AssertSql( - """ -@p0='"Dg=="' (Nullable = false) (DbType = Object) -@p1='"GRo="' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestByteCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestByteCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_char() - { - // PostgreSQL does not support the 0 char in text - var exception = await Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_char()); - var pgException = Assert.IsType(exception.InnerException); - Assert.Equal("22P05", pgException.SqlState); // untranslatable_character - } - - public override async Task Edit_single_property_collection_of_datetime() - { - await base.Edit_single_property_collection_of_datetime(); - - AssertSql( - """ -@p0='["2000-01-01T12:34:56","3000-01-01T12:34:56","3000-01-01T12:34:56"]' (Nullable = false) (DbType = Object) -@p1='["2000-01-01T12:34:56","3000-01-01T12:34:56","3000-01-01T12:34:56"]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTimeCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTimeCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_datetimeoffset() - { - await base.Edit_single_property_collection_of_datetimeoffset(); - - AssertSql( - """ -@p0='["3000-01-01T12:34:56-04:00"]' (Nullable = false) (DbType = Object) -@p1='["3000-01-01T12:34:56-04:00"]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateTimeOffsetCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDateTimeOffsetCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_decimal() - { - await base.Edit_single_property_collection_of_decimal(); - - AssertSql( - """ -@p0='[-13579.01]' (Nullable = false) (DbType = Object) -@p1='[-13579.01]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDecimalCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDecimalCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_double() - { - await base.Edit_single_property_collection_of_double(); - - AssertSql( - """ -@p0='[-1.23456789,1.23456789,0,-1.23579]' (Nullable = false) (DbType = Object) -@p1='[-1.23456789,1.23456789,0,-1.23579]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDoubleCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDoubleCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_guid() - { - await base.Edit_single_property_collection_of_guid(); - - AssertSql( - """ -@p0='["12345678-1234-4321-5555-987654321000"]' (Nullable = false) (DbType = Object) -@p1='["12345678-1234-4321-5555-987654321000"]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestGuidCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestGuidCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_int16() - { - await base.Edit_single_property_collection_of_int16(); - - AssertSql( - """ -@p0='[-3234]' (Nullable = false) (DbType = Object) -@p1='[-3234]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt16Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestInt16Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_int32() - { - await base.Edit_single_property_collection_of_int32(); - - AssertSql( - """ -@p0='[-3234]' (Nullable = false) (DbType = Object) -@p1='[-3234]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestInt32Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_int64() - { - await base.Edit_single_property_collection_of_int64(); - - AssertSql( - """ -@p0='[]' (Nullable = false) (DbType = Object) -@p1='[]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestInt64Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestInt64Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_signed_byte() - { - await base.Edit_single_property_collection_of_signed_byte(); - - AssertSql( - """ -@p0='[-108]' (Nullable = false) (DbType = Object) -@p1='[-108]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSignedByteCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestSignedByteCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_single() - { - await base.Edit_single_property_collection_of_single(); - - AssertSql( - """ -@p0='[-1.234,-1.234]' (Nullable = false) (DbType = Object) -@p1='[0,-1.234]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestSingleCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestSingleCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_timespan() - { - await base.Edit_single_property_collection_of_timespan(); - - AssertSql( - """ -@p0='["10:09:08.007","10:01:01.007"]' (Nullable = false) (DbType = Object) -@p1='["10:01:01.007","-09:50:51.993"]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeSpanCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeSpanCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_dateonly() - { - await base.Edit_single_property_collection_of_dateonly(); - - AssertSql( - """ -@p0='["3234-01-23","0001-01-07"]' (Nullable = false) (DbType = Object) -@p1='["0001-01-07","4321-01-21"]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestDateOnlyCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestDateOnlyCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_timeonly() - { - await base.Edit_single_property_collection_of_timeonly(); - - AssertSql( - """ -@p0='["13:42:23.0000000","01:01:07.0000000"]' (Nullable = false) (DbType = Object) -@p1='["01:01:07.0000000","07:17:27.0000000"]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeOnlyCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeOnlyCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_uint16() - { - await base.Edit_single_property_collection_of_uint16(); - - AssertSql( - """ -@p0='[1534]' (Nullable = false) (DbType = Object) -@p1='[1534]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt16Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt16Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_uint32() - { - await base.Edit_single_property_collection_of_uint32(); - - AssertSql( - """ -@p0='[1237775789]' (Nullable = false) (DbType = Object) -@p1='[1237775789]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt32Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_uint64() - { - await base.Edit_single_property_collection_of_uint64(); - - AssertSql( - """ -@p0='[1234555555123456789]' (Nullable = false) (DbType = Object) -@p1='[1234555555123456789]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestUnsignedInt64Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestUnsignedInt64Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_int32() - { - await base.Edit_single_property_collection_of_nullable_int32(); - - AssertSql( - """ -@p0='[null,77]' (Nullable = false) (DbType = Object) -@p1='[null,-2147483648,0,null,2147483647,null,77,null]' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_int32_set_to_null() - { - await base.Edit_single_property_collection_of_nullable_int32_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableInt32Collection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableInt32Collection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_enum() - { - await base.Edit_single_property_collection_of_enum(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_enum_with_int_converter() - { - await base.Edit_single_property_collection_of_enum_with_int_converter(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_enum() - { - await base.Edit_single_property_collection_of_nullable_enum(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_enum_set_to_null() - { - await base.Edit_single_property_collection_of_nullable_enum_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_enum_with_int_converter() - { - await base.Edit_single_property_collection_of_nullable_enum_with_int_converter(); - - AssertSql( - """ -@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) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_enum_with_int_converter_set_to_null() - { - await base.Edit_single_property_collection_of_nullable_enum_with_int_converter_set_to_null(); - - AssertSql( - """ -@p0='null' (Nullable = false) (DbType = Object) -@p1='null' (Nullable = false) (DbType = Object) -@p2='1' - -UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestNullableEnumWithIntConverterCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestNullableEnumWithIntConverterCollection}', @p1) -WHERE "Id" = @p2; -""", - // - """ -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 -"""); - } - - public override async Task Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls_set_to_null() - { - await base.Edit_single_property_collection_of_nullable_enum_with_converter_that_handles_nulls_set_to_null(); - - 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 j."Id" = 1 -LIMIT 2 -"""); - } - - // https://github.com/dotnet/efcore/pull/31831/files#r1393411950 - public override Task Add_and_update_top_level_optional_owned_collection_to_JSON(bool? value) - => Assert.ThrowsAsync(() => base.Add_and_update_top_level_optional_owned_collection_to_JSON(value)); - - public override async Task Add_and_update_nested_optional_primitive_collection(bool? value) - { - // PostgreSQL does not support the 0 char in text - var exception = await Assert.ThrowsAsync(() => base.Edit_single_property_collection_of_char()); - var pgException = Assert.IsType(exception.InnerException); - Assert.Equal("22P05", pgException.SqlState); // untranslatable_character - } - - #region Skipped tests because of unsupported list type outside of JSON - - // The following tests fail because the properties they access are ignored in the model (see OnModelCreating below). - // We do not yet support arbitrary list types outside of JSON. - public override Task Edit_single_property_relational_collection_of_bool() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_bool()); - - public override Task Edit_single_property_relational_collection_of_byte() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_byte()); - - public override Task Edit_single_property_relational_collection_of_char() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_char()); - - public override Task Edit_single_property_relational_collection_of_datetimeoffset() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_datetimeoffset()); - - public override Task Edit_single_property_relational_collection_of_double() - => Assert.ThrowsAsync(() => base.Edit_single_property_relational_collection_of_double()); - - public override Task Edit_single_property_relational_collection_of_enum() - => Assert.ThrowsAsync(() => base.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()); - - 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( - () => 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()); - - public override Task Edit_single_property_relational_collection_of_uint16() - => Assert.ThrowsAsync(() => base.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(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class JsonUpdateNpgsqlFixture : JsonUpdateFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - { - base.ConfigureConventions(configurationBuilder); - - // The tests seed Unspecified DateTimes, but our default mapping for DateTime is timestamptz, which requires UTC. - // Map these properties to "timestamp without time zone". - configurationBuilder.Properties().HaveColumnType("timestamp without time zone"); - configurationBuilder.Properties>().HaveColumnType("timestamp without time zone[]"); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - // The following are ignored since we do not support mapping IList (as opposed to array/List) on regular properties - // (since that's not supported at the ADO.NET layer). However, we do support IList inside JSON documents, since that doesn't - // rely on ADO.NET support. - modelBuilder.Entity( - b => - { - b.Ignore(j => j.TestEnumCollection); - b.Ignore(j => j.TestUnsignedInt16Collection); - b.Ignore(j => j.TestNullableEnumCollection); - b.Ignore(j => j.TestNullableEnumWithIntConverterCollection); - b.Ignore(j => j.TestCharacterCollection); - b.Ignore(j => j.TestNullableInt32Collection); - b.Ignore(j => j.TestUnsignedInt64Collection); - - b.Ignore(j => j.TestByteCollection); - b.Ignore(j => j.TestBooleanCollection); - b.Ignore(j => j.TestDateTimeOffsetCollection); - 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/Update/NonSharedModelUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/NonSharedModelUpdatesNpgsqlTest.cs deleted file mode 100644 index 9d4a5206b1..0000000000 --- a/test/EFCore.PG.FunctionalTests/Update/NonSharedModelUpdatesNpgsqlTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.EntityFrameworkCore.Update; - -public class NonSharedModelUpdatesNpgsqlTest(NonSharedFixture fixture) : NonSharedModelUpdatesTestBase(fixture) -{ - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/Update/StoreValueGenerationNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/StoreValueGenerationNpgsqlTest.cs deleted file mode 100644 index 5c6d43fc87..0000000000 --- a/test/EFCore.PG.FunctionalTests/Update/StoreValueGenerationNpgsqlTest.cs +++ /dev/null @@ -1,417 +0,0 @@ -using System.Text; -using Microsoft.EntityFrameworkCore.TestModels.StoreValueGenerationModel; - -namespace Microsoft.EntityFrameworkCore.Update; - -public class StoreValueGenerationNpgsqlTest : StoreValueGenerationTestBase< - StoreValueGenerationNpgsqlTest.StoreValueGenerationNpgsqlFixture> -{ - public StoreValueGenerationNpgsqlTest(StoreValueGenerationNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - fixture.TestSqlLoggerFactory.Clear(); - fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - protected override bool ShouldCreateImplicitTransaction( - EntityState firstOperationType, - EntityState? secondOperationType, - GeneratedValues generatedValues, - bool withSameEntityType) - => secondOperationType is not null; - - protected override int ShouldExecuteInNumberOfCommands( - EntityState firstOperationType, - EntityState? secondOperationType, - GeneratedValues generatedValues, - bool withDatabaseGenerated) - => 1; - - #region Single operation - - public override async Task Add_with_generated_values(bool async) - { - await base.Add_with_generated_values(async); - - AssertSql( - """ -@p0='1000' - -INSERT INTO "WithSomeDatabaseGenerated" ("Data2") -VALUES (@p0) -RETURNING "Id", "Data1"; -"""); - } - - public override async Task Add_with_no_generated_values(bool async) - { - await base.Add_with_no_generated_values(async); - - AssertSql( - """ -@p0='100' -@p1='1000' -@p2='1000' - -INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") -VALUES (@p0, @p1, @p2); -"""); - } - - public override async Task Add_with_all_generated_values(bool async) - { - await base.Add_with_all_generated_values(async); - - AssertSql( - """ -INSERT INTO "WithAllDatabaseGenerated" -DEFAULT VALUES -RETURNING "Id", "Data1", "Data2"; -"""); - } - - public override async Task Modify_with_generated_values(bool async) - { - await base.Modify_with_generated_values(async); - - AssertSql( - """ -@p1='1' -@p0='1000' - -UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p0 -WHERE "Id" = @p1 -RETURNING "Data1"; -"""); - } - - public override async Task Modify_with_no_generated_values(bool async) - { - await base.Modify_with_no_generated_values(async); - - AssertSql( - """ -@p2='1' -@p0='1000' -@p1='1000' - -UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p0, "Data2" = @p1 -WHERE "Id" = @p2; -"""); - } - - public override async Task Delete(bool async) - { - await base.Delete(async); - - AssertSql( - """ -@p0='1' - -DELETE FROM "WithSomeDatabaseGenerated" -WHERE "Id" = @p0; -"""); - } - - #endregion Single operation - - #region Two operations with same entity type - - public override async Task Add_Add_with_same_entity_type_and_generated_values(bool async) - { - await base.Add_Add_with_same_entity_type_and_generated_values(async); - - AssertSql( - """ -@p0='1000' -@p1='1001' - -INSERT INTO "WithSomeDatabaseGenerated" ("Data2") -VALUES (@p0) -RETURNING "Id", "Data1"; -INSERT INTO "WithSomeDatabaseGenerated" ("Data2") -VALUES (@p1) -RETURNING "Id", "Data1"; -"""); - } - - public override async Task Add_Add_with_same_entity_type_and_no_generated_values(bool async) - { - await base.Add_Add_with_same_entity_type_and_no_generated_values(async); - - AssertSql( - """ -@p0='100' -@p1='1000' -@p2='1000' -@p3='101' -@p4='1001' -@p5='1001' - -INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") -VALUES (@p0, @p1, @p2); -INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") -VALUES (@p3, @p4, @p5); -"""); - } - - public override async Task Add_Add_with_same_entity_type_and_all_generated_values(bool async) - { - await base.Add_Add_with_same_entity_type_and_all_generated_values(async); - - AssertSql( - """ -INSERT INTO "WithAllDatabaseGenerated" -DEFAULT VALUES -RETURNING "Id", "Data1", "Data2"; -INSERT INTO "WithAllDatabaseGenerated" -DEFAULT VALUES -RETURNING "Id", "Data1", "Data2"; -"""); - } - - public override async Task Modify_Modify_with_same_entity_type_and_generated_values(bool async) - { - await base.Modify_Modify_with_same_entity_type_and_generated_values(async); - - AssertSql( - """ -@p1='1' -@p0='1000' -@p3='2' -@p2='1001' - -UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p0 -WHERE "Id" = @p1 -RETURNING "Data1"; -UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p2 -WHERE "Id" = @p3 -RETURNING "Data1"; -"""); - } - - public override async Task Modify_Modify_with_same_entity_type_and_no_generated_values(bool async) - { - await base.Modify_Modify_with_same_entity_type_and_no_generated_values(async); - - AssertSql( - """ -@p2='1' -@p0='1000' -@p1='1000' -@p5='2' -@p3='1001' -@p4='1001' - -UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p0, "Data2" = @p1 -WHERE "Id" = @p2; -UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p3, "Data2" = @p4 -WHERE "Id" = @p5; -"""); - } - - public override async Task Delete_Delete_with_same_entity_type(bool async) - { - await base.Delete_Delete_with_same_entity_type(async); - - AssertSql( - """ -@p0='1' -@p1='2' - -DELETE FROM "WithSomeDatabaseGenerated" -WHERE "Id" = @p0; -DELETE FROM "WithSomeDatabaseGenerated" -WHERE "Id" = @p1; -"""); - } - - #endregion Two operations with same entity type - - #region Two operations with different entity types - - public override async Task Add_Add_with_different_entity_types_and_generated_values(bool async) - { - await base.Add_Add_with_different_entity_types_and_generated_values(async); - - AssertSql( - """ -@p0='1000' -@p1='1001' - -INSERT INTO "WithSomeDatabaseGenerated" ("Data2") -VALUES (@p0) -RETURNING "Id", "Data1"; -INSERT INTO "WithSomeDatabaseGenerated2" ("Data2") -VALUES (@p1) -RETURNING "Id", "Data1"; -"""); - } - - public override async Task Add_Add_with_different_entity_types_and_no_generated_values(bool async) - { - await base.Add_Add_with_different_entity_types_and_no_generated_values(async); - - AssertSql( - """ -@p0='100' -@p1='1000' -@p2='1000' -@p3='101' -@p4='1001' -@p5='1001' - -INSERT INTO "WithNoDatabaseGenerated" ("Id", "Data1", "Data2") -VALUES (@p0, @p1, @p2); -INSERT INTO "WithNoDatabaseGenerated2" ("Id", "Data1", "Data2") -VALUES (@p3, @p4, @p5); -"""); - } - - public override async Task Add_Add_with_different_entity_types_and_all_generated_values(bool async) - { - await base.Add_Add_with_different_entity_types_and_all_generated_values(async); - - AssertSql( - """ -INSERT INTO "WithAllDatabaseGenerated" -DEFAULT VALUES -RETURNING "Id", "Data1", "Data2"; -INSERT INTO "WithAllDatabaseGenerated2" -DEFAULT VALUES -RETURNING "Id", "Data1", "Data2"; -"""); - } - - public override async Task Modify_Modify_with_different_entity_types_and_generated_values(bool async) - { - await base.Modify_Modify_with_different_entity_types_and_generated_values(async); - - AssertSql( - """ -@p1='1' -@p0='1000' -@p3='2' -@p2='1001' - -UPDATE "WithSomeDatabaseGenerated" SET "Data2" = @p0 -WHERE "Id" = @p1 -RETURNING "Data1"; -UPDATE "WithSomeDatabaseGenerated2" SET "Data2" = @p2 -WHERE "Id" = @p3 -RETURNING "Data1"; -"""); - } - - public override async Task Modify_Modify_with_different_entity_types_and_no_generated_values(bool async) - { - await base.Modify_Modify_with_different_entity_types_and_no_generated_values(async); - - AssertSql( - """ -@p2='1' -@p0='1000' -@p1='1000' -@p5='2' -@p3='1001' -@p4='1001' - -UPDATE "WithNoDatabaseGenerated" SET "Data1" = @p0, "Data2" = @p1 -WHERE "Id" = @p2; -UPDATE "WithNoDatabaseGenerated2" SET "Data1" = @p3, "Data2" = @p4 -WHERE "Id" = @p5; -"""); - } - - public override async Task Delete_Delete_with_different_entity_types(bool async) - { - await base.Delete_Delete_with_different_entity_types(async); - - AssertSql( - """ -@p0='1' -@p1='2' - -DELETE FROM "WithSomeDatabaseGenerated" -WHERE "Id" = @p0; -DELETE FROM "WithSomeDatabaseGenerated2" -WHERE "Id" = @p1; -"""); - } - - #endregion Two operations with different entity types - - public class StoreValueGenerationNpgsqlFixture : StoreValueGenerationFixtureBase - { - private string? _cleanDataSql; - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - foreach (var name in new[] - { - nameof(StoreValueGenerationContext.WithSomeDatabaseGenerated), - nameof(StoreValueGenerationContext.WithSomeDatabaseGenerated2), - nameof(StoreValueGenerationContext.WithAllDatabaseGenerated), - nameof(StoreValueGenerationContext.WithAllDatabaseGenerated2) - }) - { - ConfigureComputedColumn(modelBuilder.SharedTypeEntity(name).Property(w => w.Data1)); - } - - foreach (var name in new[] - { - nameof(StoreValueGenerationContext.WithAllDatabaseGenerated), - nameof(StoreValueGenerationContext.WithAllDatabaseGenerated2) - }) - { - ConfigureComputedColumn(modelBuilder.SharedTypeEntity(name).Property(w => w.Data2)); - } - - void ConfigureComputedColumn(PropertyBuilder builder) - { - if (TestEnvironment.PostgresVersion >= new Version(12, 0)) - { - // PG 12+ supports computed columns, but only stored (must be explicitly specified) - builder.Metadata.SetIsStored(true); - } - else - { - // Before PG 12, disable computed columns (but leave OnAddOrUpdate) - builder - .HasComputedColumnSql(null) - .HasDefaultValue(100) - .Metadata - .ValueGenerated = ValueGenerated.OnAddOrUpdate; - } - } - } - - public override void CleanData() - { - using var context = CreateContext(); - context.Database.ExecuteSqlRaw(_cleanDataSql ??= GetCleanDataSql()); - } - - private string GetCleanDataSql() - { - var context = CreateContext(); - var builder = new StringBuilder(); - - var helper = context.GetService(); - var tables = context.Model.GetEntityTypes() - .SelectMany(e => e.GetTableMappings().Select(m => helper.DelimitIdentifier(m.Table.Name, m.Table.Schema))); - - foreach (var table in tables) - { - builder.AppendLine($"TRUNCATE TABLE {table} RESTART IDENTITY;"); - } - - return builder.ToString(); - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Update/StoredProcedureUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/StoredProcedureUpdateNpgsqlTest.cs deleted file mode 100644 index 0655904619..0000000000 --- a/test/EFCore.PG.FunctionalTests/Update/StoredProcedureUpdateNpgsqlTest.cs +++ /dev/null @@ -1,565 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; - -namespace Microsoft.EntityFrameworkCore.Update; - -[MinimumPostgresVersion(14, 0)] -public class StoredProcedureUpdateNpgsqlTest(NonSharedFixture fixture) : StoredProcedureUpdateTestBase(fixture) -{ - public override async Task Insert_with_output_parameter(bool async) - { - await base.Insert_with_output_parameter( - async, - """ -CREATE PROCEDURE "Entity_Insert"(name text, OUT id int) LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; -END $$ -"""); - - AssertSql( - """ -@p0='New' - -CALL "Entity_Insert"(@p0, NULL); -"""); - } - - public override async Task Insert_twice_with_output_parameter(bool async) - { - await base.Insert_twice_with_output_parameter( - async, - """ -CREATE PROCEDURE "Entity_Insert"(name text, OUT id int) LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; -END $$ -"""); - - AssertSql( - """ -@p0='New1' -@p1='New2' - -CALL "Entity_Insert"(@p0, NULL); -CALL "Entity_Insert"(@p1, NULL); -"""); - } - - public override async Task Insert_with_result_column(bool async) - { - var exception = - await Assert.ThrowsAsync(() => base.Insert_with_result_column(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Insert"), exception.Message); - } - - public override async Task Insert_with_two_result_columns(bool async) - { - var exception = - await Assert.ThrowsAsync(() => base.Insert_with_two_result_columns(async, createSprocSql: "")); - - Assert.Equal( - NpgsqlStrings.StoredProcedureResultColumnsNotSupported( - nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Insert"), exception.Message); - } - - public override async Task Insert_with_output_parameter_and_result_column(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Insert_with_output_parameter_and_result_column(async, createSprocSql: "")); - - Assert.Equal( - NpgsqlStrings.StoredProcedureResultColumnsNotSupported( - nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Insert"), exception.Message); - } - - public override async Task Update(bool async) - { - await base.Update( - async, - """ -CREATE PROCEDURE "Entity_Update"(id int, name text) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "Entity" SET "Name" = name WHERE "Id" = id; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1='Updated' - -CALL "Entity_Update"(@p0, @p1); -"""); - } - - public override async Task Update_partial(bool async) - { - await base.Update_partial( - async, - """ -CREATE PROCEDURE "EntityWithAdditionalProperty_Update"(id int, name text, additional_property int) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "EntityWithAdditionalProperty" SET "Name" = name, "AdditionalProperty" = additional_property WHERE "Id" = id; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1='Updated' -@p2='8' - -CALL "EntityWithAdditionalProperty_Update"(@p0, @p1, @p2); -"""); - } - - public override async Task Update_with_output_parameter_and_rows_affected_result_column(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Update_with_output_parameter_and_rows_affected_result_column(async, createSprocSql: "")); - - Assert.Equal( - NpgsqlStrings.StoredProcedureResultColumnsNotSupported( - nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Update"), exception.Message); - } - - public override async Task Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Update_with_output_parameter_and_rows_affected_result_column_concurrency_failure(async, createSprocSql: "")); - - Assert.Equal( - NpgsqlStrings.StoredProcedureResultColumnsNotSupported( - nameof(EntityWithAdditionalProperty), nameof(EntityWithAdditionalProperty) + "_Update"), exception.Message); - } - - public override async Task Delete(bool async) - { - await base.Delete( - async, - """ -CREATE PROCEDURE "Entity_Delete"(id int) LANGUAGE plpgsql AS $$ -BEGIN - DELETE FROM "Entity" WHERE "Id" = id; -END $$ -"""); - - AssertSql( - """ -@p0='1' - -CALL "Entity_Delete"(@p0); -"""); - } - - public override async Task Delete_and_insert(bool async) - { - await base.Delete_and_insert( - async, - """ -CREATE PROCEDURE "Entity_Insert"(name text, OUT id int) LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; -END $$; - -CREATE PROCEDURE "Entity_Delete"(id int) LANGUAGE plpgsql AS $$ -BEGIN - DELETE FROM "Entity" WHERE "Id" = id; -END $$; -"""); - - AssertSql( - """ -@p0='1' -@p1='Entity2' - -CALL "Entity_Delete"(@p0); -CALL "Entity_Insert"(@p1, NULL); -"""); - } - - public override async Task Rows_affected_parameter(bool async) - { - await base.Rows_affected_parameter( - async, - """ -CREATE PROCEDURE "Entity_Update"(id int, name text, OUT rows_affected int) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "Entity" SET "Name" = name WHERE "Id" = id; - GET DIAGNOSTICS rows_affected = ROW_COUNT; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1='Updated' - -CALL "Entity_Update"(@p0, @p1, NULL); -"""); - } - - public override async Task Rows_affected_parameter_and_concurrency_failure(bool async) - { - await base.Rows_affected_parameter_and_concurrency_failure( - async, - """ -CREATE PROCEDURE "Entity_Update"(id int, name text, OUT rows_affected int) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "Entity" SET "Name" = name WHERE "Id" = id; - GET DIAGNOSTICS rows_affected = ROW_COUNT; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1='Updated' - -CALL "Entity_Update"(@p0, @p1, NULL); -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Rows_affected_parameter_with_another_output_parameter(bool async) - { - // PG doesn't supposed non-stored computed columns, so we need to duplicate the test code - var createSprocSql = - """ -CREATE PROCEDURE "EntityWithAdditionalProperty_Update"(id int, OUT rows_affected int, OUT additional_property int, name text) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "EntityWithAdditionalProperty" SET "Name" = name WHERE "Id" = id RETURNING "AdditionalProperty" INTO additional_property; - GET DIAGNOSTICS rows_affected = ROW_COUNT; -END $$ -"""; - - var contextFactory = await InitializeAsync( - modelBuilder => modelBuilder.Entity() - .UpdateUsingStoredProcedure( - nameof(EntityWithAdditionalProperty) + "_Update", - spb => spb - .HasOriginalValueParameter(w => w.Id) - .HasRowsAffectedParameter() - .HasParameter(w => w.AdditionalProperty, pb => pb.IsOutput()) - .HasParameter(w => w.Name)) - .Property(w => w.AdditionalProperty).HasComputedColumnSql("8", stored: true), - seed: ctx => CreateStoredProcedures(ctx, createSprocSql)); - - await using var context = contextFactory.CreateContext(); - - var entity = new EntityWithAdditionalProperty { Name = "Initial" }; - context.Set().Add(entity); - await context.SaveChangesAsync(); - - ClearLog(); - - entity.Name = "Updated"; - entity.AdditionalProperty = 10; - - if (async) - { - await context.SaveChangesAsync(); - } - else - { - // ReSharper disable once MethodHasAsyncOverload - context.SaveChanges(); - } - - using (TestSqlLoggerFactory.SuspendRecordingEvents()) - { - var loadedEntity = await context.Set().SingleAsync(w => w.Id == entity.Id); - Assert.Equal("Updated", loadedEntity.Name); - Assert.Equal(8, loadedEntity.AdditionalProperty); - } - - AssertSql( - """ -@p0='1' -@p1='Updated' - -CALL "EntityWithAdditionalProperty_Update"(@p0, NULL, NULL, @p1); -"""); - } - - public override async Task Rows_affected_result_column(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Rows_affected_result_column(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); - } - - public override async Task Rows_affected_result_column_and_concurrency_failure(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Rows_affected_result_column_and_concurrency_failure(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); - } - - public override async Task Rows_affected_return_value(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Rows_affected_return_value(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureReturnValueNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); - } - - public override async Task Rows_affected_return_value_and_concurrency_failure(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Rows_affected_return_value(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureReturnValueNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); - } - - public override async Task Store_generated_concurrency_token_as_in_out_parameter(bool async) - { - await base.Store_generated_concurrency_token_as_in_out_parameter( - async, - """ -CREATE PROCEDURE "Entity_Update"(id int, INOUT concurrency_token xid, name text, OUT rows_affected int) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "Entity" SET "Name" = name WHERE "Id" = id AND xmin = concurrency_token RETURNING xmin INTO concurrency_token; - GET DIAGNOSTICS rows_affected = ROW_COUNT; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1=NULL (Direction = InputOutput) (DbType = Object) -@p2='Updated' - -CALL "Entity_Update"(@p0, @p1, @p2, NULL); -"""); - } - - public override async Task Store_generated_concurrency_token_as_two_parameters(bool async) - { - await base.Store_generated_concurrency_token_as_two_parameters( - async, - """ -CREATE PROCEDURE "Entity_Update"(id int, concurrency_token_in xid, name text, OUT concurrency_token_out xid, OUT rows_affected int) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "Entity" SET "Name" = name WHERE "Id" = id AND xmin = concurrency_token_in RETURNING xmin INTO concurrency_token_out; - GET DIAGNOSTICS rows_affected = ROW_COUNT; -END $$ -"""); - - // Can't assert SQL baseline as usual because the concurrency token changes - Assert.Equal( - """ -@p2='Updated' - -CALL "Entity_Update"(@p0, @p1, @p2, NULL, NULL); -""", - TestSqlLoggerFactory.Sql.Substring(TestSqlLoggerFactory.Sql.IndexOf("@p2", StringComparison.Ordinal)), - ignoreLineEndingDifferences: true); - } - - public override async Task User_managed_concurrency_token(bool async) - { - await base.User_managed_concurrency_token( - async, - """ -CREATE PROCEDURE "EntityWithAdditionalProperty_Update"(id int, concurrency_token_original int, name text, concurrency_token_current int, OUT rows_affected int) LANGUAGE plpgsql AS $$ -BEGIN - UPDATE "EntityWithAdditionalProperty" SET "Name" = name, "AdditionalProperty" = concurrency_token_current WHERE "Id" = id AND "AdditionalProperty" = concurrency_token_original; - GET DIAGNOSTICS rows_affected = ROW_COUNT; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1='8' -@p2='Updated' -@p3='9' - -CALL "EntityWithAdditionalProperty_Update"(@p0, @p1, @p2, @p3, NULL); -"""); - } - - public override async Task Original_and_current_value_on_non_concurrency_token(bool async) - { - await base.Original_and_current_value_on_non_concurrency_token( - async, - """ -CREATE PROCEDURE "Entity_Update"(id int, name_current text, name_original text) LANGUAGE plpgsql AS $$ -BEGIN - IF name_current <> name_original THEN - UPDATE "Entity" SET "Name" = name_current WHERE "Id" = id; - END IF; -END $$ -"""); - - AssertSql( - """ -@p0='1' -@p1='Updated' -@p2='Initial' - -CALL "Entity_Update"(@p0, @p1, @p2); -"""); - } - - public override async Task Input_or_output_parameter_with_input(bool async) - { - await base.Input_or_output_parameter_with_input( - async, - """ -CREATE PROCEDURE "Entity_Insert"(OUT id int, INOUT name text) LANGUAGE plpgsql AS $$ -BEGIN - IF name IS NULL THEN - INSERT INTO "Entity" ("Name") VALUES ('Some default value') RETURNING "Id", "Name" INTO id, name; - ELSE - INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; - name = NULL; - END IF; -END $$ -"""); - - AssertSql( - """ -@p0='1' (Direction = InputOutput) (DbType = String) - -CALL "Entity_Insert"(NULL, @p0); -"""); - } - - public override async Task Input_or_output_parameter_with_output(bool async) - { - await base.Input_or_output_parameter_with_output( - async, - """ -CREATE PROCEDURE "Entity_Insert"(OUT id int, INOUT name text) LANGUAGE plpgsql AS $$ -BEGIN - IF name IS NULL THEN - INSERT INTO "Entity" ("Name") VALUES ('Some default value') RETURNING "Id", "Name" INTO id, name; - ELSE - INSERT INTO "Entity" ("Name") VALUES (name) RETURNING "Id" INTO id; - name = NULL; - END IF; -END $$ -"""); - - AssertSql( - """ -@p0='1' (Direction = InputOutput) (DbType = String) - -CALL "Entity_Insert"(NULL, @p0); -"""); - } - - public override async Task Tph(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Rows_affected_result_column(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); - } - - public override async Task Tpt(bool async) - { - var exception = - await Assert.ThrowsAsync( - () => base.Rows_affected_result_column(async, createSprocSql: "")); - - Assert.Equal(NpgsqlStrings.StoredProcedureResultColumnsNotSupported(nameof(Entity), nameof(Entity) + "_Update"), exception.Message); - } - - public override async Task Tpt_mixed_sproc_and_non_sproc(bool async) - { - await base.Tpt_mixed_sproc_and_non_sproc( - async, - """ -CREATE PROCEDURE "Parent_Insert"(OUT id int, name text) LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO "Parent" ("Name") VALUES (name) RETURNING "Id" INTO id; -END $$ -"""); - - AssertSql( - """ -@p0='Child' - -CALL "Parent_Insert"(NULL, @p0); -""", - // - """ -@p1='1' -@p2='8' - -INSERT INTO "Child1" ("Id", "Child1Property") -VALUES (@p1, @p2); -"""); - } - - public override async Task Tpc(bool async) - { - await base.Tpc( - async, - """ -CREATE PROCEDURE "Child1_Insert"(OUT id int, name text, child1_property int) LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO "Child1" ("Name", "Child1Property") VALUES (name, child1_property) RETURNING "Id" INTO id; -END $$ -"""); - - AssertSql( - """ -@p0='Child' -@p1='8' - -CALL "Child1_Insert"(NULL, @p0, @p1); -"""); - } - - public override async Task Non_sproc_followed_by_sproc_commands_in_the_same_batch(bool async) - { - await base.Non_sproc_followed_by_sproc_commands_in_the_same_batch( - async, - """ -CREATE PROCEDURE "EntityWithAdditionalProperty_Insert"(name text, OUT id int, additional_property int) LANGUAGE plpgsql AS $$ -BEGIN - INSERT INTO "EntityWithAdditionalProperty" ("Name", "AdditionalProperty") VALUES (name, additional_property) RETURNING "Id" INTO id; -END $$ -"""); - - AssertSql( - """ -@p2='1' -@p0='2' -@p3='1' -@p1='Entity1_Modified' -@p4='Entity2' -@p5='0' - -UPDATE "EntityWithAdditionalProperty" SET "AdditionalProperty" = @p0, "Name" = @p1 -WHERE "Id" = @p2 AND "AdditionalProperty" = @p3; -CALL "EntityWithAdditionalProperty_Insert"(@p4, NULL, @p5); -"""); - } - - protected override void ConfigureStoreGeneratedConcurrencyToken(EntityTypeBuilder entityTypeBuilder, string propertyName) - => entityTypeBuilder.Property(propertyName) - .HasColumnName("xmin") - .HasColumnType("xid") - .ValueGeneratedOnAddOrUpdate() - .IsConcurrencyToken(); - - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; -} diff --git a/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs deleted file mode 100644 index 87e2bc634e..0000000000 --- a/test/EFCore.PG.FunctionalTests/UpdatesNpgsqlTest.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; - -namespace Microsoft.EntityFrameworkCore; - -public class UpdatesNpgsqlTest : UpdatesRelationalTestBase -{ - // ReSharper disable once UnusedParameter.Local - public UpdatesNpgsqlTest(UpdatesNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - } - - public override void Identifiers_are_generated_correctly() - { - using var context = CreateContext(); - - var entityType = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly - ))!; - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatI~", - entityType.GetTableName()); - Assert.Equal( - "PK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameTh~", - entityType.GetKeys().Single().GetName()); - Assert.Equal( - "FK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameTh~", - entityType.GetForeignKeys().Single().GetConstraintName()); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameTh~", - entityType.GetIndexes().Single().GetDatabaseName()); - - var entityType2 = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails - ))!; - - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThat~1", - entityType2.GetTableName()); - Assert.Equal( - "PK_LoginDetails", - entityType2.GetKeys().Single().GetName()); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsU~", - entityType2.GetProperties().ElementAt(1).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIs~1", - entityType2.GetProperties().ElementAt(2).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameT~1", - entityType2.GetIndexes().Single().GetDatabaseName()); - } - - public class UpdatesNpgsqlFixture : UpdatesRelationalFixture - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.HasPostgresExtension("uuid-ossp"); - - 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().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 deleted file mode 100644 index 7fa28f4e15..0000000000 --- a/test/EFCore.PG.FunctionalTests/ValueConvertersEndToEndNpgsqlTest.cs +++ /dev/null @@ -1,219 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class ValueConvertersEndToEndNpgsqlTest(ValueConvertersEndToEndNpgsqlTest.ValueConvertersEndToEndNpgsqlFixture fixture) - : ValueConvertersEndToEndTestBase(fixture) -{ - [ConditionalTheory(Skip = "DateTime and DateTimeOffset, https://github.com/dotnet/efcore/issues/26068")] - public override Task Can_insert_and_read_back_with_conversions(int[] valueOrder) - => base.Can_insert_and_read_back_with_conversions(valueOrder); - - [ConditionalTheory] - [InlineData(nameof(ConvertingEntity.BoolAsChar), "character(1)", false)] - [InlineData(nameof(ConvertingEntity.BoolAsNullableChar), "character(1)", false)] - [InlineData(nameof(ConvertingEntity.BoolAsString), "character varying(3)", false)] - [InlineData(nameof(ConvertingEntity.BoolAsInt), "integer", false)] - [InlineData(nameof(ConvertingEntity.BoolAsNullableString), "character varying(3)", false)] - [InlineData(nameof(ConvertingEntity.BoolAsNullableInt), "integer", false)] - [InlineData(nameof(ConvertingEntity.IntAsLong), "bigint", false)] - [InlineData(nameof(ConvertingEntity.IntAsNullableLong), "bigint", false)] - [InlineData(nameof(ConvertingEntity.BytesAsString), "text", false)] - [InlineData(nameof(ConvertingEntity.BytesAsNullableString), "text", false)] - [InlineData(nameof(ConvertingEntity.CharAsString), "character varying(1)", false)] - [InlineData(nameof(ConvertingEntity.CharAsNullableString), "character varying(1)", false)] - [InlineData(nameof(ConvertingEntity.DateTimeOffsetToBinary), "bigint", false)] - [InlineData(nameof(ConvertingEntity.DateTimeOffsetToNullableBinary), "bigint", false)] - [InlineData(nameof(ConvertingEntity.DateTimeOffsetToString), "character varying(48)", false)] - [InlineData(nameof(ConvertingEntity.DateTimeOffsetToNullableString), "character varying(48)", false)] - [InlineData(nameof(ConvertingEntity.DateTimeToBinary), "bigint", false)] - [InlineData(nameof(ConvertingEntity.DateTimeToNullableBinary), "bigint", false)] - [InlineData(nameof(ConvertingEntity.DateTimeToString), "character varying(48)", false)] - [InlineData(nameof(ConvertingEntity.DateTimeToNullableString), "character varying(48)", false)] - [InlineData(nameof(ConvertingEntity.EnumToString), "text", false)] - [InlineData(nameof(ConvertingEntity.EnumToNullableString), "text", false)] - [InlineData(nameof(ConvertingEntity.EnumToNumber), "bigint", false)] - [InlineData(nameof(ConvertingEntity.EnumToNullableNumber), "bigint", false)] - [InlineData(nameof(ConvertingEntity.GuidToString), "character varying(36)", false)] - [InlineData(nameof(ConvertingEntity.GuidToNullableString), "character varying(36)", false)] - [InlineData(nameof(ConvertingEntity.GuidToBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.GuidToNullableBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.IPAddressToString), "character varying(45)", false)] - [InlineData(nameof(ConvertingEntity.IPAddressToNullableString), "character varying(45)", false)] - [InlineData(nameof(ConvertingEntity.IPAddressToBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.IPAddressToNullableBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.PhysicalAddressToString), "character varying(20)", false)] - [InlineData(nameof(ConvertingEntity.PhysicalAddressToNullableString), "character varying(20)", false)] - [InlineData(nameof(ConvertingEntity.PhysicalAddressToBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.PhysicalAddressToNullableBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.NumberToString), "character varying(64)", false)] - [InlineData(nameof(ConvertingEntity.NumberToNullableString), "character varying(64)", false)] - [InlineData(nameof(ConvertingEntity.NumberToBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.NumberToNullableBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.StringToBool), "boolean", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableBool), "boolean", false)] - [InlineData(nameof(ConvertingEntity.StringToBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableBytes), "bytea", false)] - [InlineData(nameof(ConvertingEntity.StringToChar), "character(1)", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableChar), "character(1)", false)] - [InlineData(nameof(ConvertingEntity.StringToDateTime), "timestamp with time zone", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableDateTime), "timestamp with time zone", false)] - // [InlineData(nameof(ConvertingEntity.StringToDateTimeOffset), "timestamp with time zone", false)] - // [InlineData(nameof(ConvertingEntity.StringToNullableDateTimeOffset), "timestamp with time zone", false)] - [InlineData(nameof(ConvertingEntity.StringToEnum), "integer", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableEnum), "integer", false)] - [InlineData(nameof(ConvertingEntity.StringToGuid), "uuid", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableGuid), "uuid", false)] - [InlineData(nameof(ConvertingEntity.StringToNumber), "smallint", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableNumber), "smallint", false)] - [InlineData(nameof(ConvertingEntity.StringToTimeSpan), "interval", false)] - [InlineData(nameof(ConvertingEntity.StringToNullableTimeSpan), "interval", false)] - [InlineData(nameof(ConvertingEntity.TimeSpanToTicks), "bigint", false)] - [InlineData(nameof(ConvertingEntity.TimeSpanToNullableTicks), "bigint", false)] - [InlineData(nameof(ConvertingEntity.TimeSpanToString), "character varying(48)", false)] - [InlineData(nameof(ConvertingEntity.TimeSpanToNullableString), "character varying(48)", false)] - [InlineData(nameof(ConvertingEntity.UriToString), "text", false)] - [InlineData(nameof(ConvertingEntity.UriToNullableString), "text", false)] - [InlineData(nameof(ConvertingEntity.NullableCharAsString), "character varying(1)", true)] - [InlineData(nameof(ConvertingEntity.NullableCharAsNullableString), "character varying(1)", true)] - [InlineData(nameof(ConvertingEntity.NullableBoolAsChar), "character(1)", true)] - [InlineData(nameof(ConvertingEntity.NullableBoolAsNullableChar), "character(1)", true)] - [InlineData(nameof(ConvertingEntity.NullableBoolAsString), "character varying(3)", true)] - [InlineData(nameof(ConvertingEntity.NullableBoolAsNullableString), "character varying(3)", true)] - [InlineData(nameof(ConvertingEntity.NullableBoolAsInt), "integer", true)] - [InlineData(nameof(ConvertingEntity.NullableBoolAsNullableInt), "integer", true)] - [InlineData(nameof(ConvertingEntity.NullableIntAsLong), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableIntAsNullableLong), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableBytesAsString), "text", true)] - [InlineData(nameof(ConvertingEntity.NullableBytesAsNullableString), "text", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToBinary), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToNullableBinary), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToString), "character varying(48)", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeOffsetToNullableString), "character varying(48)", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeToBinary), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeToNullableBinary), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeToString), "character varying(48)", true)] - [InlineData(nameof(ConvertingEntity.NullableDateTimeToNullableString), "character varying(48)", true)] - [InlineData(nameof(ConvertingEntity.NullableEnumToString), "text", true)] - [InlineData(nameof(ConvertingEntity.NullableEnumToNullableString), "text", true)] - [InlineData(nameof(ConvertingEntity.NullableEnumToNumber), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableEnumToNullableNumber), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableGuidToString), "character varying(36)", true)] - [InlineData(nameof(ConvertingEntity.NullableGuidToNullableString), "character varying(36)", true)] - [InlineData(nameof(ConvertingEntity.NullableGuidToBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableGuidToNullableBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableIPAddressToString), "character varying(45)", true)] - [InlineData(nameof(ConvertingEntity.NullableIPAddressToNullableString), "character varying(45)", true)] - [InlineData(nameof(ConvertingEntity.NullableIPAddressToBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableIPAddressToNullableBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToString), "character varying(20)", true)] - [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToNullableString), "character varying(20)", true)] - [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullablePhysicalAddressToNullableBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableNumberToString), "character varying(64)", true)] - [InlineData(nameof(ConvertingEntity.NullableNumberToNullableString), "character varying(64)", true)] - [InlineData(nameof(ConvertingEntity.NullableNumberToBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableNumberToNullableBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToBool), "boolean", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableBool), "boolean", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableBytes), "bytea", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToChar), "character(1)", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableChar), "character(1)", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToDateTime), "timestamp with time zone", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableDateTime), "timestamp with time zone", true)] - //[InlineData(nameof(ConvertingEntity.NullableStringToDateTimeOffset), "timestamp with time zone", true)] - //[InlineData(nameof(ConvertingEntity.NullableStringToNullableDateTimeOffset), "timestamp with time zone", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToEnum), "integer", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableEnum), "integer", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToGuid), "uuid", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableGuid), "uuid", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNumber), "smallint", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableNumber), "smallint", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToTimeSpan), "interval", true)] - [InlineData(nameof(ConvertingEntity.NullableStringToNullableTimeSpan), "interval", true)] - [InlineData(nameof(ConvertingEntity.NullableTimeSpanToTicks), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableTimeSpanToNullableTicks), "bigint", true)] - [InlineData(nameof(ConvertingEntity.NullableTimeSpanToString), "character varying(48)", true)] - [InlineData(nameof(ConvertingEntity.NullableTimeSpanToNullableString), "character varying(48)", true)] - [InlineData(nameof(ConvertingEntity.NullableUriToString), "text", true)] - [InlineData(nameof(ConvertingEntity.NullableUriToNullableString), "text", true)] - [InlineData(nameof(ConvertingEntity.NullStringToNonNullString), "text", false)] - [InlineData(nameof(ConvertingEntity.NonNullStringToNullString), "text", true)] - public virtual void Properties_with_conversions_map_to_appropriately_null_columns( - string propertyName, - string databaseType, - bool isNullable) - { - using var context = CreateContext(); - - var property = context.Model.FindEntityType(typeof(ConvertingEntity))!.FindProperty(propertyName); - - Assert.Equal(databaseType, property!.GetColumnType()); - Assert.Equal(isNullable, property!.IsNullable); - } - - [ConditionalFact] - public async Task Can_insert_and_read_back_with_value_converted_array() - { - await using var ctx = CreateContext(); - - var entity = new ValueConvertedArrayEntity { Values = [new(8), new(9)] }; - ctx.Add(entity); - await ctx.SaveChangesAsync(); - - var id = entity.Id; - ctx.ChangeTracker.Clear(); - - entity = await ctx.Set().SingleAsync(v => v.Id == id); - Assert.Equal(8, entity.Values[0].Value); - Assert.Equal(9, entity.Values[1].Value); - } - - public class ValueConvertersEndToEndNpgsqlFixture : ValueConvertersEndToEndFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity( - b => - { - // We map DateTimeOffset to PG 'timestamp with time zone', which doesn't store the time zone - so lossless - // round-tripping is impossible. - b.Ignore(e => e.StringToDateTimeOffset); - b.Ignore(e => e.StringToNullableDateTimeOffset); - b.Ignore(e => e.NullableStringToDateTimeOffset); - b.Ignore(e => e.NullableStringToNullableDateTimeOffset); - }); - - // Add some Npgsql-specific value conversion scenarios - modelBuilder.Entity() - .PrimitiveCollection(x => x.Values) - .ElementType(e => e.HasConversion(typeof(IntWrapperConverter))); - } - - private class IntWrapperConverter() : ValueConverter(iw => iw.Value, i => new IntWrapper(i)); - } - - public class ValueConvertedArrayEntity - { - public int Id { get; set; } - public IntWrapper[] Values { get; set; } = null!; - } - - public class IntWrapper(int value) : IEquatable - { - public int Value { get; } = value; - - public bool Equals(IntWrapper? other) - => other is not null && Value == other.Value; - - public override bool Equals(object? obj) - => obj is IntWrapper other && Equals(other); - - public override int GetHashCode() - => Value.GetHashCode(); - } -} diff --git a/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs deleted file mode 100644 index 5c0a677971..0000000000 --- a/test/EFCore.PG.FunctionalTests/WithConstructorsNpgsqlTest.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Microsoft.EntityFrameworkCore; - -public class WithConstructorsNpgsqlTest(WithConstructorsNpgsqlTest.WithConstructorsNpgsqlFixture fixture) - : WithConstructorsTestBase(fixture) -{ - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public class WithConstructorsNpgsqlFixture : WithConstructorsFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => NpgsqlTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity().HasNoKey().ToSqlQuery(""" - SELECT * FROM "Blog" - """); - } - } -} diff --git a/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs b/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs deleted file mode 100644 index ec9cff9863..0000000000 --- a/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs +++ /dev/null @@ -1,412 +0,0 @@ -using Microsoft.EntityFrameworkCore.Storage.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal; - -public class NpgsqlAnnotationCodeGeneratorTest -{ - #region Identity / sequence / HiLo - - [Fact] - public void GenerateFluentApi_value_generation() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.Entity( - "Post", - x => - { - x.Property("IdentityByDefault").UseIdentityByDefaultColumn(); - x.Property("IdentityAlways").UseIdentityAlwaysColumn(); - x.Property("Serial").UseSerialColumn(); - x.Property("None").Metadata.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.None); - }); - - // Note: both serial and identity-by-default columns are considered by-convention - we don't want - // to assume that the PostgreSQL version of the scaffolded database necessarily determines the - // version of the database that the scaffolded model will target. This makes life difficult for - // models with mixed strategies but that's an edge case. - - var entity = (IEntityType)modelBuilder.Model.FindEntityType("Post"); - - var property = entity.GetProperties().Single(p => p.Name == "IdentityByDefault"); - var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - generator.RemoveAnnotationsHandledByConventions(property, annotations); - Assert.Empty(annotations); - var result = generator.GenerateFluentApiCalls(property, property.GetAnnotations().ToDictionary(a => a.Name, a => a)) - .Single(); - Assert.Equal(nameof(NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn), result.Method); - Assert.Empty(result.Arguments); - - property = entity.GetProperties().Single(p => p.Name == "IdentityAlways"); - annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - generator.RemoveAnnotationsHandledByConventions(property, annotations); - Assert.Contains(annotations, kv => kv.Key == NpgsqlAnnotationNames.ValueGenerationStrategy); - result = generator.GenerateFluentApiCalls(property, annotations).Single(); - Assert.Equal(nameof(NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn), result.Method); - Assert.Empty(result.Arguments); - - property = entity.GetProperties().Single(p => p.Name == "Serial"); - annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - generator.RemoveAnnotationsHandledByConventions(property, annotations); - Assert.Empty(annotations); - result = generator.GenerateFluentApiCalls(property, property.GetAnnotations().ToDictionary(a => a.Name, a => a)) - .Single(); - Assert.Equal(nameof(NpgsqlPropertyBuilderExtensions.UseSerialColumn), result.Method); - Assert.Empty(result.Arguments); - - property = entity.GetProperties().Single(p => p.Name == "None"); - annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - generator.RemoveAnnotationsHandledByConventions(property, annotations); - Assert.Contains(annotations, kv => kv.Key == NpgsqlAnnotationNames.ValueGenerationStrategy); - result = generator.GenerateFluentApiCalls(property, property.GetAnnotations().ToDictionary(a => a.Name, a => a)) - .Single(); - Assert.Equal(nameof(PropertyBuilder.HasAnnotation), result.Method); - Assert.Collection( - result.Arguments, - a => Assert.Equal(NpgsqlAnnotationNames.ValueGenerationStrategy, a), - a => Assert.Equal(NpgsqlValueGenerationStrategy.None, a)); - } - - [Fact] - public void GenerateFluentApi_identity_sequence_options() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.Entity( - "Post", - x => - { - x.Property("Id") - .UseIdentityByDefaultColumn() - .HasIdentityOptions( - startValue: 5, - incrementBy: 2, - minValue: 3, - maxValue: 2000, - cyclic: true, - numbersToCache: 10); - }); - - var property = (IProperty)modelBuilder.Model.FindEntityType("Post").GetProperties() - .Single(p => p.Name == "Id"); - var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - generator.RemoveAnnotationsHandledByConventions(property, annotations); - Assert.Contains(annotations, kv => kv.Key == NpgsqlAnnotationNames.IdentityOptions); - var result = generator.GenerateFluentApiCalls(property, annotations).Single(); - Assert.Equal(nameof(NpgsqlPropertyBuilderExtensions.HasIdentityOptions), result.Method); - Assert.Equal(5L, result.Arguments[0]); - Assert.Equal(2L, result.Arguments[1]); - Assert.Equal(3L, result.Arguments[2]); - Assert.Equal(2000L, result.Arguments[3]); - Assert.Equal(true, result.Arguments[4]); - Assert.Equal(10L, result.Arguments[5]); - } - - [ConditionalFact] - public void GenerateFluentApi_IModel_works_with_IdentityByDefault() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.UseIdentityByDefaultColumns(); - - var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(((IModel)modelBuilder.Model), annotations).Single(); - - Assert.Equal("UseIdentityByDefaultColumns", result.Method); - Assert.Equal("NpgsqlModelBuilderExtensions", result.DeclaringType); - - Assert.Empty(result.Arguments); - } - - [ConditionalFact] - public void GenerateFluentApi_IProperty_works_with_IdentityByDefault() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.Entity("Post", x => x.Property("Id").UseIdentityByDefaultColumn()); - var property = (IProperty)modelBuilder.Model.FindEntityType("Post").FindProperty("Id"); - - var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(property, annotations).Single(); - - Assert.Equal("UseIdentityByDefaultColumn", result.Method); - Assert.Equal("NpgsqlPropertyBuilderExtensions", result.DeclaringType); - - Assert.Empty(result.Arguments); - } - - [ConditionalFact] - public void GenerateFluentApi_IModel_works_with_IdentityAlways() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.UseIdentityAlwaysColumns(); - - var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(((IModel)modelBuilder.Model), annotations).Single(); - - Assert.Equal("UseIdentityAlwaysColumns", result.Method); - Assert.Equal("NpgsqlModelBuilderExtensions", result.DeclaringType); - - Assert.Empty(result.Arguments); - } - - [ConditionalFact] - public void GenerateFluentApi_IProperty_works_with_IdentityAlways() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.Entity("Post", x => x.Property("Id").UseIdentityAlwaysColumn()); - var property = (IProperty)modelBuilder.Model.FindEntityType("Post").FindProperty("Id"); - - var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(property, annotations).Single(); - - Assert.Equal("UseIdentityAlwaysColumn", result.Method); - Assert.Equal("NpgsqlPropertyBuilderExtensions", result.DeclaringType); - - Assert.Empty(result.Arguments); - } - - [ConditionalFact] - public void GenerateFluentApi_IModel_works_with_HiLo() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.UseHiLo("HiLoIndexName", "HiLoIndexSchema"); - - var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls((IModel)modelBuilder.Model, annotations).Single(); - - Assert.Equal("UseHiLo", result.Method); - Assert.Equal("NpgsqlModelBuilderExtensions", result.DeclaringType); - - Assert.Collection( - result.Arguments, - name => Assert.Equal("HiLoIndexName", name), - schema => Assert.Equal("HiLoIndexSchema", schema)); - } - - [ConditionalFact] - public void GenerateFluentApi_IProperty_works_with_HiLo() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.Entity("Post", x => x.Property("Id").UseHiLo("HiLoIndexName", "HiLoIndexSchema")); - var property = (IProperty)modelBuilder.Model.FindEntityType("Post").FindProperty("Id"); - - var annotations = property.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(property, annotations).Single(); - - Assert.Equal("UseHiLo", result.Method); - Assert.Equal("NpgsqlPropertyBuilderExtensions", result.DeclaringType); - - Assert.Collection( - result.Arguments, - name => Assert.Equal("HiLoIndexName", name), - schema => Assert.Equal("HiLoIndexSchema", schema)); - } - - #endregion Identity / sequence / HiLo - - #region PostgreSQL extensions - - [ConditionalFact] - public void Extension() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.HasPostgresExtension("postgis"); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension)); - - Assert.Collection(result.Arguments, name => Assert.Equal("postgis", name)); - } - - [ConditionalFact] - public void Extension_with_schema() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.HasPostgresExtension("some_schema", "postgis"); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension)); - - Assert.Collection( - result.Arguments, - schema => Assert.Equal("some_schema", schema), - name => Assert.Equal("postgis", name)); - } - - [ConditionalFact] - public void Extension_with_null_schema() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - modelBuilder.HasPostgresExtension(null, "postgis"); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresExtension)); - - Assert.Collection(result.Arguments, name => Assert.Equal("postgis", name)); - } - - #endregion PostgreSQL extensions - - #region Enum - - [ConditionalFact] - public void Enum() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - - var enumLabels = new[] { "someValue1", "someValue2" }; - modelBuilder.HasPostgresEnum("some_enum", enumLabels); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum)); - - Assert.Collection( - result.Arguments, - name => Assert.Equal("some_enum", name), - labels => Assert.Equal(enumLabels, labels)); - } - - [ConditionalFact] - public void Enum_with_schema() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - - var enumLabels = new[] { "someValue1", "someValue2" }; - modelBuilder.HasPostgresEnum("some_schema", "some_enum", enumLabels); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum)); - - Assert.Collection( - result.Arguments, - schema => Assert.Equal("some_schema", schema), - name => Assert.Equal("some_enum", name), - labels => Assert.Equal(enumLabels, labels)); - } - - [ConditionalFact] - public void Enum_with_null_schema() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - - var enumLabels = new[] { "someValue1", "someValue2" }; - modelBuilder.HasPostgresEnum(schema: null, "some_enum", enumLabels); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresEnum)); - - Assert.Collection( - result.Arguments, - name => Assert.Equal("some_enum", name), - labels => Assert.Equal(enumLabels, labels)); - } - - #endregion Enum - - #region Range - - [ConditionalFact] - public void Range() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - - modelBuilder.HasPostgresRange("some_range", "some_subtype"); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresRange)); - - Assert.Collection( - result.Arguments, - name => Assert.Equal("some_range", name), - subtype => Assert.Equal("some_subtype", subtype)); - } - - [ConditionalFact] - public void Range_with_schema() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - - modelBuilder.HasPostgresRange("some_schema", "some_range", "some_subtype"); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresRange)); - - Assert.Collection( - result.Arguments, - schema => Assert.Equal("some_schema", schema), - name => Assert.Equal("some_range", name), - subtype => Assert.Equal("some_subtype", subtype), - canonicalFunction => Assert.Null(canonicalFunction), - subtypeOpClass => Assert.Null(subtypeOpClass), - collation => Assert.Null(collation), - subtypeDiff => Assert.Null(subtypeDiff)); - } - - [ConditionalFact] - public void Range_with_null_schema() - { - var generator = CreateGenerator(); - var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); - - modelBuilder.HasPostgresRange(schema: null, "some_range", "some_subtype"); - - var model = (IModel)modelBuilder.Model; - var annotations = model.GetAnnotations().ToDictionary(a => a.Name, a => a); - var result = generator.GenerateFluentApiCalls(model, annotations) - .Single(c => c.Method == nameof(NpgsqlModelBuilderExtensions.HasPostgresRange)); - - Assert.Collection( - result.Arguments, - name => Assert.Equal("some_range", name), - subtype => Assert.Equal("some_subtype", subtype)); - } - - #endregion Range - - private NpgsqlAnnotationCodeGenerator CreateGenerator() - => new( - new AnnotationCodeGeneratorDependencies( - new NpgsqlTypeMappingSource( - new TypeMappingSourceDependencies( - new ValueConverterSelector(new ValueConverterSelectorDependencies()), - new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - [] - ), - 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 deleted file mode 100644 index 8facd65aba..0000000000 --- a/test/EFCore.PG.Tests/EFCore.PG.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Npgsql.EntityFrameworkCore.PostgreSQL.Tests - Npgsql.EntityFrameworkCore.PostgreSQL - disable - - - - - - - - - - - - - - - - - - diff --git a/test/EFCore.PG.Tests/Metadata/Conventions/NpgsqlPostgresModelFinalizingConventionTest.cs b/test/EFCore.PG.Tests/Metadata/Conventions/NpgsqlPostgresModelFinalizingConventionTest.cs deleted file mode 100644 index 70a593aa89..0000000000 --- a/test/EFCore.PG.Tests/Metadata/Conventions/NpgsqlPostgresModelFinalizingConventionTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -public class NpgsqlPostgresModelFinalizingConventionTest -{ - [Fact] - public void RowVersion_properties_get_mapped_to_xmin() - { - var modelBuilder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - modelBuilder.Entity().Property(b => b.RowVersion).IsRowVersion(); - var model = modelBuilder.FinalizeModel(); - - var entityType = model.FindEntityType(typeof(Blog))!; - var property = entityType.FindProperty(nameof(Blog.RowVersion))!; - - Assert.Equal("xmin", property.GetColumnName()); - Assert.Equal("xid", property.GetColumnType()); - } - - private class Blog - { - public int Id { get; set; } - - [Timestamp] - public uint RowVersion { get; set; } - } -} diff --git a/test/EFCore.PG.Tests/Metadata/Conventions/NpgsqlValueGenerationStrategyConventionTest.cs b/test/EFCore.PG.Tests/Metadata/Conventions/NpgsqlValueGenerationStrategyConventionTest.cs deleted file mode 100644 index 3569119663..0000000000 --- a/test/EFCore.PG.Tests/Metadata/Conventions/NpgsqlValueGenerationStrategyConventionTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; - -public class NpgsqlValueGenerationStrategyConventionTest -{ - [Fact] - public void Annotations_are_added_when_conventional_model_builder_is_used() - { - var model = NpgsqlTestHelpers.Instance.CreateConventionBuilder().Model; - - var annotations = model.GetAnnotations().OrderBy(a => a.Name).ToList(); - Assert.Equal(3, annotations.Count); - - Assert.Equal(NpgsqlAnnotationNames.ValueGenerationStrategy, annotations.First().Name); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, annotations.First().Value); - } - - [Fact] - public void Annotations_are_added_when_conventional_model_builder_is_used_with_sequences() - { - var model = NpgsqlTestHelpers.Instance.CreateConventionBuilder() - .UseHiLo() - .Model; - - model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); - - var annotations = model.GetAnnotations().OrderBy(a => a.Name).ToList(); - Assert.Equal(4, annotations.Count); - - Assert.Equal(NpgsqlAnnotationNames.HiLoSequenceName, annotations[0].Name); - Assert.Equal(NpgsqlModelExtensions.DefaultHiLoSequenceName, annotations[0].Value); - - Assert.Equal(NpgsqlAnnotationNames.ValueGenerationStrategy, annotations[1].Name); - Assert.Equal(NpgsqlValueGenerationStrategy.SequenceHiLo, annotations[1].Value); - - Assert.Equal(RelationalAnnotationNames.MaxIdentifierLength, annotations[2].Name); - - Assert.Equal( - RelationalAnnotationNames.Sequences, - annotations[3].Name); - Assert.NotNull(annotations[3].Value); - } -} diff --git a/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs b/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs deleted file mode 100644 index b4efac2bd4..0000000000 --- a/test/EFCore.PG.Tests/Metadata/NpgsqlBuilderExtensionsTest.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -public class NpgsqlBuilderExtensionsTest -{ - [Fact] - public void CockroachDbInterleaveInParent() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder.Entity() - .ToTable("customers", "my_schema") - .UseCockroachDbInterleaveInParent(typeof(Customer), ["col_a", "col_b"]); - - var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); - var interleaveInParent = entityType.GetCockroachDbInterleaveInParent(); - - Assert.Equal("my_schema", interleaveInParent.ParentTableSchema); - Assert.Equal("customers", interleaveInParent.ParentTableName); - var interleavePrefix = interleaveInParent.InterleavePrefix; - Assert.Equal(2, interleavePrefix.Count); - Assert.Equal("col_a", interleavePrefix[0]); - Assert.Equal("col_b", interleavePrefix[1]); - } - - [Fact] - public void Can_set_identity_sequence_options_on_property() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder - .Entity() - .Property(e => e.Id) - .UseIdentityByDefaultColumn() - .HasIdentityOptions( - startValue: 5, - incrementBy: 2, - minValue: 3, - maxValue: 2000, - cyclic: true, - numbersToCache: 10); - - var model = modelBuilder.Model; - var property = model.FindEntityType(typeof(Customer)).FindProperty("Id"); - - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, property.GetValueGenerationStrategy()); - Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); - Assert.Equal(5, property.GetIdentityStartValue()); - Assert.Equal(3, property.GetIdentityMinValue()); - Assert.Equal(2000, property.GetIdentityMaxValue()); - Assert.Equal(2, property.GetIdentityIncrementBy()); - Assert.True(property.GetIdentityIsCyclic()); - Assert.Equal(10, property.GetIdentityNumbersToCache()); - - Assert.Null(model.FindSequence(NpgsqlModelExtensions.DefaultHiLoSequenceName)); - Assert.Null(model.FindSequence(NpgsqlModelExtensions.DefaultHiLoSequenceName)); - } - - protected virtual ModelBuilder CreateConventionModelBuilder() - => NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - - private class Customer - { - public int Id { get; set; } - public string Name { get; set; } - - public IEnumerable Orders { get; set; } - } - - private class Order - { - public int OrderId { get; set; } - - public int CustomerId { get; set; } - public Customer Customer { get; set; } - - public OrderDetails Details { get; set; } - } - - private class OrderDetails - { - public int Id { get; set; } - - public int OrderId { get; set; } - public Order Order { get; set; } - } -} diff --git a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs b/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs deleted file mode 100644 index 91aa830760..0000000000 --- a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataBuilderExtensionsTest.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -public class NpgsqlInternalMetadataBuilderExtensionsTest -{ - private IConventionModelBuilder CreateBuilder() - => new InternalModelBuilder(new Model()); - - [ConditionalFact] - public void Can_access_model() - { - var builder = CreateBuilder(); - - Assert.NotNull( - builder - .HasValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo)); - Assert.Equal(NpgsqlValueGenerationStrategy.SequenceHiLo, builder.Metadata.GetValueGenerationStrategy()); - - Assert.NotNull( - builder - .HasValueGenerationStrategy(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, fromDataAnnotation: true)); - Assert.Equal( - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, builder.Metadata.GetValueGenerationStrategy()); - - Assert.Null( - builder - .HasValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo)); - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, builder.Metadata.GetValueGenerationStrategy()); - - Assert.Equal( - 1, builder.Metadata.GetAnnotations().Count( - a => a.Name.StartsWith(NpgsqlAnnotationNames.Prefix, StringComparison.Ordinal))); - } - - [ConditionalFact] - public void Can_access_entity_type() - { - var typeBuilder = CreateBuilder().Entity(typeof(Splot)); - - Assert.NotNull(typeBuilder.IsUnlogged()); - Assert.True(typeBuilder.Metadata.GetIsUnlogged()); - - Assert.NotNull(typeBuilder.IsUnlogged(false, fromDataAnnotation: true)); - Assert.False(typeBuilder.Metadata.GetIsUnlogged()); - - Assert.Null(typeBuilder.IsUnlogged()); - Assert.False(typeBuilder.Metadata.GetIsUnlogged()); - - Assert.Equal( - 1, typeBuilder.Metadata.GetAnnotations().Count( - a => a.Name.StartsWith(NpgsqlAnnotationNames.Prefix, StringComparison.Ordinal))); - } - - [ConditionalFact] - public void Can_access_property() - { - var propertyBuilder = CreateBuilder() - .Entity(typeof(Splot)) - .Property(typeof(int), "Id"); - - Assert.NotNull(propertyBuilder.HasHiLoSequence("Splew", null)); - Assert.Equal("Splew", propertyBuilder.Metadata.GetHiLoSequenceName()); - - Assert.NotNull(propertyBuilder.HasHiLoSequence("Splow", null, fromDataAnnotation: true)); - Assert.Equal("Splow", propertyBuilder.Metadata.GetHiLoSequenceName()); - - Assert.Null(propertyBuilder.HasHiLoSequence("Splod", null)); - Assert.Equal("Splow", propertyBuilder.Metadata.GetHiLoSequenceName()); - - Assert.Equal( - 1, propertyBuilder.Metadata.GetAnnotations().Count( - a => a.Name.StartsWith(NpgsqlAnnotationNames.Prefix, StringComparison.Ordinal))); - } - - [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([idProperty]); - - Assert.NotNull(indexBuilder.HasMethod("gin")); - Assert.Equal("gin", indexBuilder.Metadata.GetMethod()); - - Assert.NotNull(indexBuilder.HasMethod("gist", fromDataAnnotation: true)); - Assert.Equal("gist", indexBuilder.Metadata.GetMethod()); - - Assert.Null(indexBuilder.HasMethod("gin")); - Assert.Equal("gist", indexBuilder.Metadata.GetMethod()); - - Assert.Equal( - 1, indexBuilder.Metadata.GetAnnotations().Count( - a => a.Name.StartsWith(NpgsqlAnnotationNames.Prefix, StringComparison.Ordinal))); - } - - [ConditionalFact] - 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([idProperty]).Metadata; - var relationshipBuilder = entityTypeBuilder.HasRelationship(entityTypeBuilder.Metadata, key); - - Assert.NotNull(relationshipBuilder.HasConstraintName("Splew")); - Assert.Equal("Splew", relationshipBuilder.Metadata.GetConstraintName()); - - Assert.NotNull(relationshipBuilder.HasConstraintName("Splow", fromDataAnnotation: true)); - Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); - - Assert.Null(relationshipBuilder.HasConstraintName("Splod")); - Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); - - Assert.Equal( - 1, relationshipBuilder.Metadata.GetAnnotations().Count( - a => a.Name.StartsWith(RelationalAnnotationNames.Prefix, StringComparison.Ordinal))); - } - - private class Splot; -} diff --git a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs b/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs deleted file mode 100644 index 9115f5f73a..0000000000 --- a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs +++ /dev/null @@ -1,536 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -public class NpgsqlMetadataExtensionsTest -{ - [ConditionalFact] - public void Can_get_and_set_column_name() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Name) - .Metadata; - - Assert.Equal("Name", property.GetColumnName()); - Assert.Null(((IConventionProperty)property).GetColumnNameConfigurationSource()); - - ((IConventionProperty)property).SetColumnName("Eman", fromDataAnnotation: true); - - Assert.Equal("Eman", property.GetColumnName()); - Assert.Equal(ConfigurationSource.DataAnnotation, ((IConventionProperty)property).GetColumnNameConfigurationSource()); - - property.SetColumnName("MyNameIs"); - - Assert.Equal("Name", property.Name); - Assert.Equal("MyNameIs", property.GetColumnName()); - Assert.Equal(ConfigurationSource.Explicit, ((IConventionProperty)property).GetColumnNameConfigurationSource()); - - property.SetColumnName(null); - - Assert.Equal("Name", property.GetColumnName()); - Assert.Null(((IConventionProperty)property).GetColumnNameConfigurationSource()); - } - - [ConditionalFact] - public void Can_get_and_set_column_key_name() - { - var modelBuilder = GetModelBuilder(); - - var key = modelBuilder - .Entity() - .HasKey(e => e.Id) - .Metadata; - - Assert.Equal("PK_Customer", key.GetName()); - - key.SetName("PrimaryKey"); - - Assert.Equal("PrimaryKey", key.GetName()); - - key.SetName("PrimarySchool"); - - Assert.Equal("PrimarySchool", key.GetName()); - - key.SetName(null); - - Assert.Equal("PK_Customer", key.GetName()); - } - - [ConditionalFact] - public void Can_get_and_set_index_method() - { - var modelBuilder = GetModelBuilder(); - - var index = modelBuilder - .Entity() - .HasIndex(e => e.Id) - .Metadata; - - Assert.Null(index.GetMethod()); - - index.SetMethod("gin"); - - Assert.Equal("gin", index.GetMethod()); - - index.SetMethod(null); - - Assert.Null(index.GetMethod()); - } - - [ConditionalFact] - public void Can_get_and_set_sequence() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - Assert.Null(model.FindSequence("Foo")); - Assert.Null(model.FindSequence("Foo")); - Assert.Null(((IModel)model).FindSequence("Foo")); - - var sequence = model.AddSequence("Foo"); - - Assert.Equal("Foo", model.FindSequence("Foo").Name); - Assert.Equal("Foo", ((IModel)model).FindSequence("Foo").Name); - Assert.Equal("Foo", model.FindSequence("Foo").Name); - Assert.Equal("Foo", ((IModel)model).FindSequence("Foo").Name); - - Assert.Equal("Foo", sequence.Name); - Assert.Null(sequence.Schema); - Assert.Equal(1, sequence.IncrementBy); - Assert.Equal(1, sequence.StartValue); - Assert.Null(sequence.MinValue); - Assert.Null(sequence.MaxValue); - Assert.Same(typeof(long), sequence.Type); - - Assert.NotNull(model.FindSequence("Foo")); - - var sequence2 = model.FindSequence("Foo"); - - sequence.StartValue = 1729; - sequence.IncrementBy = 11; - sequence.MinValue = 2001; - sequence.MaxValue = 2010; - sequence.Type = typeof(int); - - Assert.Equal("Foo", sequence.Name); - Assert.Null(sequence.Schema); - Assert.Equal(11, sequence.IncrementBy); - Assert.Equal(1729, sequence.StartValue); - Assert.Equal(2001, sequence.MinValue); - Assert.Equal(2010, sequence.MaxValue); - Assert.Same(typeof(int), sequence.Type); - - Assert.Equal(sequence2.Name, sequence.Name); - Assert.Equal(sequence2.Schema, sequence.Schema); - Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); - Assert.Equal(sequence2.StartValue, sequence.StartValue); - Assert.Equal(sequence2.MinValue, sequence.MinValue); - Assert.Equal(sequence2.MaxValue, sequence.MaxValue); - Assert.Same(sequence2.Type, sequence.Type); - } - - [ConditionalFact] - public void Can_get_and_set_sequence_with_schema_name() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - Assert.Null(model.FindSequence("Foo", "Smoo")); - Assert.Null(model.FindSequence("Foo", "Smoo")); - Assert.Null(((IModel)model).FindSequence("Foo", "Smoo")); - - var sequence = model.AddSequence("Foo", "Smoo"); - - Assert.Equal("Foo", model.FindSequence("Foo", "Smoo").Name); - Assert.Equal("Foo", ((IModel)model).FindSequence("Foo", "Smoo").Name); - Assert.Equal("Foo", model.FindSequence("Foo", "Smoo").Name); - Assert.Equal("Foo", ((IModel)model).FindSequence("Foo", "Smoo").Name); - - Assert.Equal("Foo", sequence.Name); - Assert.Equal("Smoo", sequence.Schema); - Assert.Equal(1, sequence.IncrementBy); - Assert.Equal(1, sequence.StartValue); - Assert.Null(sequence.MinValue); - Assert.Null(sequence.MaxValue); - Assert.Same(typeof(long), sequence.Type); - - Assert.NotNull(model.FindSequence("Foo", "Smoo")); - - var sequence2 = model.FindSequence("Foo", "Smoo"); - - sequence.StartValue = 1729; - sequence.IncrementBy = 11; - sequence.MinValue = 2001; - sequence.MaxValue = 2010; - sequence.Type = typeof(int); - - Assert.Equal("Foo", sequence.Name); - Assert.Equal("Smoo", sequence.Schema); - Assert.Equal(11, sequence.IncrementBy); - Assert.Equal(1729, sequence.StartValue); - Assert.Equal(2001, sequence.MinValue); - Assert.Equal(2010, sequence.MaxValue); - Assert.Same(typeof(int), sequence.Type); - - Assert.Equal(sequence2.Name, sequence.Name); - Assert.Equal(sequence2.Schema, sequence.Schema); - Assert.Equal(sequence2.IncrementBy, sequence.IncrementBy); - Assert.Equal(sequence2.StartValue, sequence.StartValue); - Assert.Equal(sequence2.MinValue, sequence.MinValue); - Assert.Equal(sequence2.MaxValue, sequence.MaxValue); - Assert.Same(sequence2.Type, sequence.Type); - } - - [ConditionalFact] - public void Can_get_multiple_sequences() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - model.AddSequence("Fibonacci"); - model.AddSequence("Golomb"); - - var sequences = model.GetSequences(); - - Assert.Equal(2, sequences.Count()); - Assert.Contains(sequences, s => s.Name == "Fibonacci"); - Assert.Contains(sequences, s => s.Name == "Golomb"); - } - - [ConditionalFact] - public void Can_get_multiple_sequences_when_overridden() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - model.AddSequence("Fibonacci").StartValue = 1; - model.FindSequence("Fibonacci").StartValue = 3; - model.AddSequence("Golomb"); - - var sequences = model.GetSequences(); - - Assert.Equal(2, sequences.Count()); - Assert.Contains(sequences, s => s.Name == "Golomb"); - - var sequence = sequences.FirstOrDefault(s => s.Name == "Fibonacci"); - Assert.NotNull(sequence); - Assert.Equal(3, sequence.StartValue); - } - - [ConditionalFact] - public void Can_get_and_set_value_generation_on_model() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - // TODO for PG9.6 testing: make this conditional - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, model.GetValueGenerationStrategy()); - - model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal(NpgsqlValueGenerationStrategy.SequenceHiLo, model.GetValueGenerationStrategy()); - - model.SetValueGenerationStrategy(null); - - Assert.Null(model.GetValueGenerationStrategy()); - } - - [ConditionalFact] - public void Can_get_and_set_default_sequence_name_on_model() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - model.SetHiLoSequenceName("Tasty.Snook"); - - Assert.Equal("Tasty.Snook", model.GetHiLoSequenceName()); - - model.SetHiLoSequenceName(null); - - Assert.Equal(NpgsqlModelExtensions.DefaultHiLoSequenceName, model.GetHiLoSequenceName()); - } - - [ConditionalFact] - public void Can_get_and_set_default_sequence_schema_on_model() - { - var modelBuilder = GetModelBuilder(); - var model = modelBuilder.Model; - - Assert.Null(model.GetHiLoSequenceSchema()); - - model.SetHiLoSequenceSchema("Tasty.Snook"); - - Assert.Equal("Tasty.Snook", model.GetHiLoSequenceSchema()); - - model.SetHiLoSequenceSchema(null); - - Assert.Null(model.GetHiLoSequenceSchema()); - } - - [ConditionalFact] - public void Can_get_and_set_value_generation_on_property() - { - var modelBuilder = GetModelBuilder(); - modelBuilder.Model.SetValueGenerationStrategy(null); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .Metadata; - - Assert.Equal(NpgsqlValueGenerationStrategy.None, property.GetValueGenerationStrategy()); - Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); - - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal(NpgsqlValueGenerationStrategy.SequenceHiLo, property.GetValueGenerationStrategy()); - Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); - - property.SetValueGenerationStrategy(null); - - Assert.Equal(NpgsqlValueGenerationStrategy.None, property.GetValueGenerationStrategy()); - Assert.Equal(ValueGenerated.OnAdd, property.ValueGenerated); - } - - [ConditionalFact] - public void Can_get_and_set_value_generation_on_nullable_property() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.NullableInt).ValueGeneratedOnAdd() - .Metadata; - - // TODO for PG9.6 testing: make this conditional - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, property.GetValueGenerationStrategy()); - - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal(NpgsqlValueGenerationStrategy.SequenceHiLo, property.GetValueGenerationStrategy()); - - property.SetValueGenerationStrategy(null); - - // TODO for PG9.6 testing: make this conditional - Assert.Equal(NpgsqlValueGenerationStrategy.IdentityByDefaultColumn, property.GetValueGenerationStrategy()); - } - - [ConditionalFact] - public void Can_get_and_set_sequence_name_on_property() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .Metadata; - - Assert.Null(property.GetHiLoSequenceName()); - Assert.Null(((IProperty)property).GetHiLoSequenceName()); - - property.SetHiLoSequenceName("Snook"); - - Assert.Equal("Snook", property.GetHiLoSequenceName()); - - property.SetHiLoSequenceName(null); - - Assert.Null(property.GetHiLoSequenceName()); - } - - [ConditionalFact] - public void Can_get_and_set_sequence_schema_on_property() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .Metadata; - - Assert.Null(property.GetHiLoSequenceSchema()); - - property.SetHiLoSequenceSchema("Tasty"); - - Assert.Equal("Tasty", property.GetHiLoSequenceSchema()); - - property.SetHiLoSequenceSchema(null); - - Assert.Null(property.GetHiLoSequenceSchema()); - } - - [ConditionalFact] - public void TryGetSequence_returns_sequence_property_is_marked_for_sequence_generation() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw"); - property.SetHiLoSequenceName("DaneelOlivaw"); - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - } - - [ConditionalFact] - public void TryGetSequence_returns_sequence_property_is_marked_for_default_generation_and_model_is_marked_for_sequence_generation() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw"); - modelBuilder.Model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - property.SetHiLoSequenceName("DaneelOlivaw"); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - } - - [ConditionalFact] - public void TryGetSequence_returns_sequence_property_is_marked_for_sequence_generation_and_model_has_name() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw"); - modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - } - - [ConditionalFact] - public void - TryGetSequence_returns_sequence_property_is_marked_for_default_generation_and_model_is_marked_for_sequence_generation_and_model_has_name() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw"); - modelBuilder.Model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - } - - [ConditionalFact] - public void TryGetSequence_with_schema_returns_sequence_property_is_marked_for_sequence_generation() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); - property.SetHiLoSequenceName("DaneelOlivaw"); - property.SetHiLoSequenceSchema("R"); - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - Assert.Equal("R", property.FindHiLoSequence().Schema); - } - - [ConditionalFact] - public void TryGetSequence_with_schema_returns_sequence_model_is_marked_for_sequence_generation() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); - modelBuilder.Model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - property.SetHiLoSequenceName("DaneelOlivaw"); - property.SetHiLoSequenceSchema("R"); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - Assert.Equal("R", property.FindHiLoSequence().Schema); - } - - [ConditionalFact] - public void TryGetSequence_with_schema_returns_sequence_property_is_marked_for_sequence_generation_and_model_has_name() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); - modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); - modelBuilder.Model.SetHiLoSequenceSchema("R"); - property.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - Assert.Equal("R", property.FindHiLoSequence().Schema); - } - - [ConditionalFact] - public void TryGetSequence_with_schema_returns_sequence_model_is_marked_for_sequence_generation_and_model_has_name() - { - var modelBuilder = GetModelBuilder(); - - var property = modelBuilder - .Entity() - .Property(e => e.Id) - .ValueGeneratedOnAdd() - .Metadata; - - modelBuilder.Model.AddSequence("DaneelOlivaw", "R"); - modelBuilder.Model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.SequenceHiLo); - modelBuilder.Model.SetHiLoSequenceName("DaneelOlivaw"); - modelBuilder.Model.SetHiLoSequenceSchema("R"); - - Assert.Equal("DaneelOlivaw", property.FindHiLoSequence().Name); - Assert.Equal("R", property.FindHiLoSequence().Schema); - } - - private static ModelBuilder GetModelBuilder() - => NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - - // ReSharper disable once ClassNeverInstantiated.Local - private class Customer - { - public int Id { get; set; } - public int? NullableInt { get; set; } - public string Name { get; set; } - public byte Byte { get; set; } - public byte? NullableByte { get; set; } - public byte[] ByteArray { get; set; } - } - - private class Order - { - public int OrderId { get; set; } - public int CustomerId { get; set; } - } -} diff --git a/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs b/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs deleted file mode 100644 index 0462716483..0000000000 --- a/test/EFCore.PG.Tests/Migrations/NpgsqlHistoryRepositoryTest.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; - -public class NpgsqlHistoryRepositoryTest -{ - [ConditionalFact] - public void GetCreateScript_works() - { - var sql = CreateHistoryRepository().GetCreateScript(); - - Assert.Equal( - """ -CREATE TABLE "__EFMigrationsHistory" ( - "MigrationId" character varying(150) NOT NULL, - "ProductVersion" character varying(32) NOT NULL, - CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") -); - -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetCreateScript_works_with_schema() - { - var sql = CreateHistoryRepository("my").GetCreateScript(); - - Assert.Equal( - """ -DO $EF$ -BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'my') THEN - CREATE SCHEMA my; - END IF; -END $EF$; -CREATE TABLE my."__EFMigrationsHistory" ( - "MigrationId" character varying(150) NOT NULL, - "ProductVersion" character varying(32) NOT NULL, - CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") -); - -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetCreateIfNotExistsScript_works() - { - var sql = CreateHistoryRepository().GetCreateIfNotExistsScript(); - - Assert.Equal( - """ -CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( - "MigrationId" character varying(150) NOT NULL, - "ProductVersion" character varying(32) NOT NULL, - CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") -); - -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetCreateIfNotExistsScript_works_with_schema() - { - var sql = CreateHistoryRepository("my").GetCreateIfNotExistsScript(); - - Assert.Equal( - """ -DO $EF$ -BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'my') THEN - CREATE SCHEMA my; - END IF; -END $EF$; -CREATE TABLE IF NOT EXISTS my."__EFMigrationsHistory" ( - "MigrationId" character varying(150) NOT NULL, - "ProductVersion" character varying(32) NOT NULL, - CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") -); - -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetDeleteScript_works() - { - var sql = CreateHistoryRepository().GetDeleteScript("Migration1"); - - Assert.Equal( - """ -DELETE FROM "__EFMigrationsHistory" -WHERE "MigrationId" = 'Migration1'; - -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetInsertScript_works() - { - var sql = CreateHistoryRepository().GetInsertScript( - new HistoryRow("Migration1", "7.0.0")); - - Assert.Equal( - """ -INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('Migration1', '7.0.0'); - -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetBeginIfNotExistsScript_works() - { - var sql = CreateHistoryRepository().GetBeginIfNotExistsScript("Migration1"); - - Assert.Equal( - """ - -DO $EF$ -BEGIN - IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = 'Migration1') THEN -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetBeginIfExistsScript_works() - { - var sql = CreateHistoryRepository().GetBeginIfExistsScript("Migration1"); - - Assert.Equal( - """ -DO $EF$ -BEGIN - IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = 'Migration1') THEN -""", sql, ignoreLineEndingDifferences: true); - } - - [ConditionalFact] - public void GetEndIfScript_works() - { - var sql = CreateHistoryRepository().GetEndIfScript(); - - Assert.Equal( - """ - END IF; -END $EF$; -""", sql, ignoreLineEndingDifferences: true); - } - - private static IHistoryRepository CreateHistoryRepository(string schema = null) - => new TestDbContext( - new DbContextOptionsBuilder() - .UseInternalServiceProvider(NpgsqlTestHelpers.Instance.CreateServiceProvider()) - .UseNpgsql( - new NpgsqlConnection("Host=localhost;Database=DummyDatabase"), - b => b.MigrationsHistoryTable(HistoryRepository.DefaultTableName, schema)) - .Options) - .GetService(); - - private class TestDbContext(DbContextOptions options) : DbContext(options) - { - public DbSet Blogs { get; set; } - - [DbFunction("TableFunction")] - public IQueryable TableFunction() - => FromExpression(() => TableFunction()); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - } - } - - private class Blog - { - public int Id { get; set; } - } - - private class TableFunction - { - public int Id { get; set; } - public int BlogId { get; set; } - public Blog Blog { get; set; } - } -} diff --git a/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs b/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs deleted file mode 100644 index e8e29ad87d..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlDatabaseFacadeTest.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -public class NpgsqlDatabaseFacadeTest -{ - [Fact] - public void IsNpgsql_when_using_OnConfiguring() - { - using var context = new NpgsqlOnConfiguringContext(); - - Assert.True(context.Database.IsNpgsql()); - } - - [Fact] - public void IsNpgsql_in_OnModelCreating_when_using_OnConfiguring() - { - using var context = new NpgsqlOnModelContext(); - var _ = context.Model; // Trigger context initialization - - Assert.True(context.IsNpgsqlSet); - } - - [Fact] - public void IsNpgsql_in_constructor_when_using_OnConfiguring() - { - using var context = new NpgsqlConstructorContext(); - var _ = context.Model; // Trigger context initialization - - Assert.True(context.IsNpgsqlSet); - } - - [Fact] - public void Cannot_use_IsNpgsql_in_OnConfiguring() - { - using var context = new NpgsqlUseInOnConfiguringContext(); - - Assert.Equal( - CoreStrings.RecursiveOnConfiguring, - Assert.Throws( - () => - { - var _ = context.Model; // Trigger context initialization - }).Message); - } - - [Fact] - public void IsNpgsql_when_using_constructor() - { - using var context = new ProviderContext( - new DbContextOptionsBuilder().UseNpgsql("Database=Maltesers").Options); - - Assert.True(context.Database.IsNpgsql()); - } - - [Fact] - public void IsNpgsql_in_OnModelCreating_when_using_constructor() - { - using var context = new ProviderOnModelContext( - new DbContextOptionsBuilder().UseNpgsql("Database=Maltesers").Options); - var _ = context.Model; // Trigger context initialization - - Assert.True(context.IsNpgsqlSet); - } - - [Fact] - public void IsNpgsql_in_constructor_when_using_constructor() - { - using var context = new ProviderConstructorContext( - new DbContextOptionsBuilder().UseNpgsql("Database=Maltesers").Options); - var _ = context.Model; // Trigger context initialization - - Assert.True(context.IsNpgsqlSet); - } - - [Fact] - public void Cannot_use_IsNpgsql_in_OnConfiguring_with_constructor() - { - using var context = new ProviderUseInOnConfiguringContext( - new DbContextOptionsBuilder().UseNpgsql("Database=Maltesers").Options); - - Assert.Equal( - CoreStrings.RecursiveOnConfiguring, - Assert.Throws( - () => - { - var _ = context.Model; // Trigger context initialization - }).Message); - } - - /* - [Fact] - public void Not_IsNpgsql_when_using_different_provider() - { - using var context = new ProviderContext( - new DbContextOptionsBuilder().UseInMemoryDatabase("Maltesers").Options); - - Assert.False(context.Database.IsNpgsql()); - }*/ - - private class ProviderContext : DbContext - { - protected ProviderContext() - { - } - - public ProviderContext(DbContextOptions options) - : base(options) - { - } - - public bool? IsNpgsqlSet { get; protected set; } - } - - private class NpgsqlOnConfiguringContext : ProviderContext - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseNpgsql("Database=Maltesers"); - } - - private class NpgsqlOnModelContext : NpgsqlOnConfiguringContext - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => IsNpgsqlSet = Database.IsNpgsql(); - } - - private class NpgsqlConstructorContext : NpgsqlOnConfiguringContext - { - // ReSharper disable once VirtualMemberCallInConstructor - public NpgsqlConstructorContext() - { - IsNpgsqlSet = Database.IsNpgsql(); - } - } - - private class NpgsqlUseInOnConfiguringContext : NpgsqlOnConfiguringContext - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - base.OnConfiguring(optionsBuilder); - - IsNpgsqlSet = Database.IsNpgsql(); - } - } - - private class ProviderOnModelContext(DbContextOptions options) : ProviderContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => IsNpgsqlSet = Database.IsNpgsql(); - } - - private class ProviderConstructorContext : ProviderContext - { - // ReSharper disable once VirtualMemberCallInConstructor - public ProviderConstructorContext(DbContextOptions options) - : base(options) - { - IsNpgsqlSet = Database.IsNpgsql(); - } - } - - private class ProviderUseInOnConfiguringContext(DbContextOptions options) : ProviderContext(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 deleted file mode 100644 index 93c749fcb2..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -public class NpgsqlDbContextOptionsExtensionsTest -{ - [ConditionalFact] - public void Can_add_extension_with_max_batch_size() - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Database=Crunchie", b => b.MaxBatchSize(123)); - - var extension = optionsBuilder.Options.Extensions.OfType().Single(); - - Assert.Equal(123, extension.MaxBatchSize); - } - - [ConditionalFact] - public void Can_add_extension_with_command_timeout() - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Database=Crunchie", b => b.CommandTimeout(30)); - - var extension = optionsBuilder.Options.Extensions.OfType().Single(); - - Assert.Equal(30, extension.CommandTimeout); - } - - [ConditionalFact] - public void Can_add_extension_with_connection_string() - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Database=Crunchie"); - - var extension = optionsBuilder.Options.Extensions.OfType().Single(); - - Assert.Equal("Database=Crunchie", extension.ConnectionString); - Assert.Null(extension.Connection); - } - - [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] - public void Can_add_extension_with_connection_string_using_generic_options(bool nullConnectionString) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(nullConnectionString ? null : "Database=Whisper"); - - var extension = optionsBuilder.Options.Extensions.OfType().Single(); - - Assert.Equal(nullConnectionString ? null : "Database=Whisper", extension.ConnectionString); - Assert.Null(extension.Connection); - } - - [ConditionalFact] - public void Can_add_extension_with_connection() - { - var optionsBuilder = new DbContextOptionsBuilder(); - var connection = new NpgsqlConnection(); - - optionsBuilder.UseNpgsql(connection); - - var extension = optionsBuilder.Options.Extensions.OfType().Single(); - - Assert.Same(connection, extension.Connection); - Assert.Null(extension.ConnectionString); - } - - [ConditionalFact] - public void Can_add_extension_with_connection_using_generic_options() - { - var optionsBuilder = new DbContextOptionsBuilder(); - var connection = new NpgsqlConnection(); - - optionsBuilder.UseNpgsql(connection); - - var extension = optionsBuilder.Options.Extensions.OfType().Single(); - - Assert.Same(connection, extension.Connection); - Assert.Null(extension.ConnectionString); - } - - [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] - public void Service_collection_extension_method_can_configure_npgsql_options(bool nullConnectionString) - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddNpgsql( - nullConnectionString ? null : "Database=Crunchie", - npgsqlOption => - { - npgsqlOption.MaxBatchSize(123); - npgsqlOption.CommandTimeout(30); - }, - dbContextOption => - { - dbContextOption.EnableDetailedErrors(); - }); - - var services = serviceCollection.BuildServiceProvider(); - - using (var serviceScope = services - .GetRequiredService() - .CreateScope()) - { - var coreOptions = serviceScope.ServiceProvider.GetRequiredService>() - .GetExtension(); - Assert.True(coreOptions.DetailedErrorsEnabled); - - var npgsqlOptions = serviceScope.ServiceProvider.GetRequiredService>() - .GetExtension(); - Assert.Equal(123, npgsqlOptions.MaxBatchSize); - Assert.Equal(30, npgsqlOptions.CommandTimeout); - Assert.Equal(nullConnectionString ? null : "Database=Crunchie", npgsqlOptions.ConnectionString); - } - } - - [ConditionalFact] - public void Varying_data_source_connection_strings_do_not_cause_multiple_service_providers() - { - for (var i = 0; i < 21; i++) - { - var optionsBuilder = new DbContextOptionsBuilder(); - var dataSource = NpgsqlDataSource.Create($"Host=localhost;Database={i};Include Error Detail=true;Username=admin;Password=admin"); - optionsBuilder.UseNpgsql(dataSource); - - using var context = new ApplicationDbContext(optionsBuilder.Options); - _ = context.Model; - } - } - - private class ApplicationDbContext(DbContextOptions options) : DbContext(options); -} diff --git a/test/EFCore.PG.Tests/NpgsqlMigrationBuilderTest.cs b/test/EFCore.PG.Tests/NpgsqlMigrationBuilderTest.cs deleted file mode 100644 index 3878a2122b..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlMigrationBuilderTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -public class NpgsqlMigrationBuilderTest -{ - [Fact] - public void IsNpgsql_when_using_Npgsql() - { - var migrationBuilder = new MigrationBuilder("Npgsql.EntityFrameworkCore.PostgreSQL"); - Assert.True(migrationBuilder.IsNpgsql()); - } - - [Fact] - public void Not_IsNpgsql_when_using_different_provider() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.InMemory"); - Assert.False(migrationBuilder.IsNpgsql()); - } -} diff --git a/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs b/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs deleted file mode 100644 index 029c02ecba..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlNetTopologySuiteApiConsistencyTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -public class NpgsqlNetTopologySuiteApiConsistencyTest( - NpgsqlNetTopologySuiteApiConsistencyTest.NpgsqlNetTopologySuiteApiConsistencyFixture fixture) - : ApiConsistencyTestBase(fixture) -{ - protected override void AddServices(ServiceCollection serviceCollection) - => serviceCollection.AddEntityFrameworkNpgsqlNetTopologySuite(); - - protected override Assembly TargetAssembly - => typeof(NpgsqlNetTopologySuiteServiceCollectionExtensions).Assembly; - - public class NpgsqlNetTopologySuiteApiConsistencyFixture : ApiConsistencyFixtureBase - { - 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 deleted file mode 100644 index d1e6798a4e..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlNodaTimeApiConsistencyTest.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -public class NpgsqlNodaTimeApiConsistencyTest(NpgsqlNodaTimeApiConsistencyTest.NpgsqlNodaTimeApiConsistencyFixture fixture) - : ApiConsistencyTestBase(fixture) -{ - protected override void AddServices(ServiceCollection serviceCollection) - => serviceCollection.AddEntityFrameworkNpgsqlNodaTime(); - - protected override Assembly TargetAssembly - => typeof(NpgsqlNodaTimeServiceCollectionExtensions).Assembly; - - public class NpgsqlNodaTimeApiConsistencyFixture : ApiConsistencyFixtureBase - { - 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 deleted file mode 100644 index 5f8f170e44..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System.Data.Common; -using System.Transactions; -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -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; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities.FakeProvider; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL; - -#nullable enable - -public class NpgsqlRelationalConnectionTest -{ - [Fact] - public void Creates_NpgsqlConnection() - { - using var connection = CreateConnection(); - - Assert.IsType(connection.DbConnection); - } - - [Fact] - public void Uses_DbDataSource_from_DbContextOptions() - { - using var dataSource = NpgsqlDataSource.Create("Host=FakeHost"); - - var serviceCollection = new ServiceCollection(); - - 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(); - Assert.Same(dataSource, relationalConnection1.DbDataSource); - - var connection1 = context1.GetService().Database.GetDbConnection(); - Assert.Equal("Host=FakeHost", connection1.ConnectionString); - - using var scope2 = serviceProvider.CreateScope(); - var context2 = scope2.ServiceProvider.GetRequiredService(); - var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService(); - Assert.Same(dataSource, relationalConnection2.DbDataSource); - - var connection2 = context2.GetService().Database.GetDbConnection(); - Assert.Equal("Host=FakeHost", connection2.ConnectionString); - } - - [Fact] - public void Uses_DbDataSource_from_application_service_provider() - { - var serviceCollection = new ServiceCollection(); - - serviceCollection - .AddNpgsqlDataSource("Host=FakeHost") - .AddDbContext(o => o.UseNpgsql()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var dataSource = serviceProvider.GetRequiredService(); - - using var scope1 = serviceProvider.CreateScope(); - var context1 = scope1.ServiceProvider.GetRequiredService(); - var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService(); - Assert.Same(dataSource, relationalConnection1.DbDataSource); - - var connection1 = context1.GetService().Database.GetDbConnection(); - Assert.Equal("Host=FakeHost", connection1.ConnectionString); - - using var scope2 = serviceProvider.CreateScope(); - var context2 = scope2.ServiceProvider.GetRequiredService(); - 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 Data_source_config_without_a_connection_string() - { - var context = new ConfigurableContext( - connectionString: null, - no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.Host = "192.168.1.1")); - var connection1 = (NpgsqlRelationalConnection)context.GetService(); - Assert.Equal("Host=192.168.1.1", connection1.ConnectionString); - Assert.NotNull(connection1.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() - { - using var connection = CreateConnection(); - using var master = connection.CreateAdminConnection(); - - Assert.Equal( - @"Host=localhost;Database=postgres;Username=some_user;Password=some_password;Pooling=False;Multiplexing=False", - master.ConnectionString); - } - - [Fact] - public void Can_create_master_connection_with_connection_string_and_alternate_admin_db() - { - var options = new DbContextOptionsBuilder() - .UseNpgsql( - @"Host=localhost;Database=NpgsqlConnectionTest;Username=some_user;Password=some_password", - b => b.UseAdminDatabase("template0")) - .Options; - - using var connection = CreateConnection(options); - using var master = connection.CreateAdminConnection(); - - Assert.Equal( - @"Host=localhost;Database=template0;Username=some_user;Password=some_password;Pooling=False;Multiplexing=False", - master.ConnectionString); - } - - [Theory] - [InlineData("false")] - [InlineData("False")] - [InlineData("FALSE")] - public void CurrentAmbientTransaction_returns_null_with_enlist_set_to_false(string falseValue) - { - var options = new DbContextOptionsBuilder() - .UseNpgsql( - @"Host=localhost;Database=NpgsqlConnectionTest;Username=some_user;Password=some_password;Enlist=" + falseValue) - .Options; - - Transaction.Current = new CommittableTransaction(); - - using var connection = CreateConnection(options); - Assert.Null(connection.CurrentAmbientTransaction); - - Transaction.Current = null; - } - - [Theory] - [InlineData(";Enlist=true")] - [InlineData("")] // Enlist is true by default - public void CurrentAmbientTransaction_returns_transaction_with_enlist_enabled(string enlist) - { - var options = new DbContextOptionsBuilder() - .UseNpgsql( - @"Host=localhost;Database=NpgsqlConnectionTest;Username=some_user;Password=some_password" + enlist) - .Options; - - var transaction = new CommittableTransaction(); - Transaction.Current = transaction; - - using var connection = CreateConnection(options); - Assert.Equal(transaction, connection.CurrentAmbientTransaction); - - Transaction.Current = null; - } - - [ConditionalFact] - public async Task CloneWith_with_connection_and_connection_string() - { - var services = NpgsqlTestHelpers.Instance.CreateContextServices( - new DbContextOptionsBuilder() - .UseNpgsql("Host=localhost;Database=DummyDatabase") - .Options); - - var relationalConnection = (NpgsqlRelationalConnection)services.GetRequiredService(); - - 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) - { - options ??= new DbContextOptionsBuilder() - .UseNpgsql(@"Host=localhost;Database=NpgsqlConnectionTest;Username=some_user;Password=some_password") - .Options; - - foreach (var extension in options.Extensions) - { - extension.Validate(options); - } - - var dbContextOptions = CreateOptions(); - - return new NpgsqlRelationalConnection( - new RelationalConnectionDependencies( - options, - new DiagnosticsLogger( - new LoggerFactory(), - new LoggingOptions(), - new DiagnosticListener("FakeDiagnosticListener"), - new NpgsqlLoggingDefinitions(), - new NullDbContextLogger()), - new RelationalConnectionDiagnosticsLogger( - new LoggerFactory(), - new LoggingOptions(), - new DiagnosticListener("FakeDiagnosticListener"), - new NpgsqlLoggingDefinitions(), - new NullDbContextLogger(), - dbContextOptions), - new NamedConnectionStringResolver(options), - new RelationalTransactionFactory( - new RelationalTransactionFactoryDependencies( - new RelationalSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()))), - new CurrentDbContext(new FakeDbContext()), - new RelationalCommandBuilderFactory( - new RelationalCommandBuilderDependencies( - new NpgsqlTypeMappingSource( - TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create(), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSingletonOptions()), - new ExceptionDetector(), - new LoggingOptions())), - new ExceptionDetector()), - new NpgsqlDataSourceManager([]), - dbContextOptions); - } - - private const string ConnectionString = "Fake Connection String"; - - private static IDbContextOptions CreateOptions(RelationalOptionsExtension? optionsExtension = null) - { - var optionsBuilder = new DbContextOptionsBuilder(); - - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder) - .AddOrUpdateExtension( - optionsExtension - ?? new FakeRelationalOptionsExtension().WithConnectionString(ConnectionString)); - - return optionsBuilder.Options; - } - - private class FakeDbContext : DbContext - { - public FakeDbContext() - { - } - - public FakeDbContext(DbContextOptions options) - : base(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 deleted file mode 100644 index e6e7b9caaf..0000000000 --- a/test/EFCore.PG.Tests/NpgsqlValueGeneratorSelectorTest.cs +++ /dev/null @@ -1,214 +0,0 @@ -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; - -public class NpgsqlValueGeneratorSelectorTest -{ - [Fact] - public void Returns_built_in_generators_for_types_setup_for_value_generation() - { - AssertGenerator("Id"); - AssertGenerator("Custom"); - AssertGenerator("Long"); - AssertGenerator("Short"); - AssertGenerator("Byte"); - AssertGenerator("NullableInt"); - AssertGenerator("NullableLong"); - AssertGenerator("NullableShort"); - AssertGenerator("NullableByte"); - AssertGenerator("Decimal"); - AssertGenerator("String"); - AssertGenerator("Guid"); - AssertGenerator("Binary"); - } - - private void AssertGenerator(string propertyName, bool setSequences = false) - { - var builder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - builder.Entity( - b => - { - b.Property(e => e.Custom).HasValueGenerator(); - b.Property(propertyName).ValueGeneratedOnAdd(); - b.HasKey(propertyName); - }); - - if (setSequences) - { - builder.UseHiLo(); - Assert.NotNull(builder.Model.FindSequence(NpgsqlModelExtensions.DefaultHiLoSequenceName)); - } - - var model = builder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(AnEntity)); - - var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - - var property = entityType.FindProperty(propertyName)!; - Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); - - Assert.IsType(generator); - } - - [ConditionalFact] - public void Returns_temp_guid_generator_when_default_sql_set() - { - var builder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - builder.Entity( - b => - { - b.Property(e => e.Guid).HasDefaultValueSql("newid()"); - b.HasKey(e => e.Guid); - }); - var model = builder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(AnEntity)); - - var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - - var property = entityType.FindProperty("Guid")!; - Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); - Assert.IsType(generator); - } - - [ConditionalFact] - public void Returns_temp_string_generator_when_default_sql_set() - { - var builder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - builder.Entity( - b => - { - b.Property(e => e.String).ValueGeneratedOnAdd().HasDefaultValueSql("Foo"); - b.HasKey(e => e.String); - }); - var model = builder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(AnEntity)); - - var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - - var property = entityType.FindProperty("String")!; - Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); - - Assert.IsType(generator); - Assert.True(generator.GeneratesTemporaryValues); - } - - [ConditionalFact] - public void Returns_temp_binary_generator_when_default_sql_set() - { - var builder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - builder.Entity( - b => - { - b.HasKey(e => e.Binary); - b.Property(e => e.Binary).HasDefaultValueSql("Foo").ValueGeneratedOnAdd(); - }); - var model = builder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(AnEntity)); - - var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - - var property = entityType.FindProperty("Binary")!; - Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); - - Assert.IsType(generator); - Assert.True(generator.GeneratesTemporaryValues); - } - - [Fact] - public void Returns_sequence_value_generators_when_configured_for_model() - { - AssertGenerator>("Id", setSequences: true); - AssertGenerator("Custom", setSequences: true); - AssertGenerator>("Long", setSequences: true); - AssertGenerator>("Short", setSequences: true); - AssertGenerator>("NullableInt", setSequences: true); - AssertGenerator>("NullableLong", setSequences: true); - AssertGenerator>("NullableShort", setSequences: true); - AssertGenerator("String", setSequences: true); - AssertGenerator("Guid", setSequences: true); - AssertGenerator("Binary", setSequences: true); - } - -// [ConditionalFact] -// public void Throws_for_unsupported_combinations() -// { -// var builder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); -// builder.Entity( -// b => -// { -// b.Property(e => e.Random).ValueGeneratedOnAdd(); -// b.HasKey(e => e.Random); -// }); -// var model = builder.FinalizeModel(); -// var entityType = model.FindEntityType(typeof(AnEntity)); -// -// var selector = InMemoryTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); -// -// Assert.Equal( -// CoreStrings.NoValueGenerator("Random", "AnEntity", "Something"), -// Assert.Throws(() => selector.Select(entityType.FindProperty("Random"), entityType)).Message); -// } - - [ConditionalFact] - public void Returns_generator_configured_on_model_when_property_is_identity() - { - var builder = NpgsqlTestHelpers.Instance.CreateConventionBuilder(); - - builder.Entity(); - - builder - .UseHiLo() - .HasSequence(NpgsqlModelExtensions.DefaultHiLoSequenceName); - - var model = builder.UseHiLo().FinalizeModel(); - var entityType = model.FindEntityType(typeof(AnEntity)); - - var selector = NpgsqlTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - - var property = entityType.FindProperty("Id")!; - Assert.True(selector.TrySelect(property, property.DeclaringType, out var generator)); - - Assert.IsType>(generator); - } - - private class AnEntity - { - // ReSharper disable UnusedMember.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - public int Id { get; set; } - public int Custom { get; set; } - public long Long { get; set; } - public short Short { get; set; } - public byte Byte { get; set; } - public char Char { get; set; } - public int? NullableInt { get; set; } - public long? NullableLong { get; set; } - public short? NullableShort { get; set; } - public byte? NullableByte { get; set; } - public char? NullableChar { get; set; } - public string String { get; set; } - public Guid Guid { get; set; } - public byte[] Binary { get; set; } - public float Float { get; set; } - public decimal Decimal { get; set; } - public int AlwaysIdentity { get; set; } - public int AlwaysSequence { get; set; } - - [NotMapped] - public Random Random { get; set; } - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class CustomValueGenerator : ValueGenerator - { - public override int Next(EntityEntry entry) - => throw new NotImplementedException(); - - public override bool GeneratesTemporaryValues - => false; - } -} diff --git a/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs b/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs deleted file mode 100644 index 1405216a88..0000000000 --- a/test/EFCore.PG.Tests/Scaffolding/NpgsqlCodeGeneratorTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.Scaffolding.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Scaffolding.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding; - -public class NpgsqlCodeGeneratorTest -{ - [Fact] - public virtual void Use_provider_method_is_generated_correctly() - { - var codeGenerator = new NpgsqlCodeGenerator( - new ProviderCodeGeneratorDependencies( - [])); - - var result = codeGenerator.GenerateUseProvider("Server=test;Username=test;Password=test;Database=test", providerOptions: null); - - Assert.Equal("UseNpgsql", result.Method); - Assert.Collection( - result.Arguments, - a => Assert.Equal("Server=test;Username=test;Password=test;Database=test", a)); - Assert.Null(result.ChainedCall); - } - - [Fact] - public virtual void Use_provider_method_is_generated_correctly_with_options() - { - var codeGenerator = new NpgsqlCodeGenerator( - new ProviderCodeGeneratorDependencies( - [])); - - var providerOptions = new MethodCallCodeFragment(_setProviderOptionMethodInfo); - - var result = codeGenerator.GenerateUseProvider("Server=test;Username=test;Password=test;Database=test", providerOptions); - - Assert.Equal("UseNpgsql", result.Method); - Assert.Collection( - result.Arguments, - a => Assert.Equal("Server=test;Username=test;Password=test;Database=test", a), - a => - { - var nestedClosure = Assert.IsType(a); - - Assert.Equal("x", nestedClosure.Parameter); - Assert.Same(providerOptions, nestedClosure.MethodCalls[0]); - }); - Assert.Null(result.ChainedCall); - } - - [ConditionalFact] - public virtual void Use_provider_method_is_generated_correctly_with_NetTopologySuite() - { - var codeGenerator = new NpgsqlCodeGenerator( - new ProviderCodeGeneratorDependencies( - [new NpgsqlNetTopologySuiteCodeGeneratorPlugin()])); - - var result = ((IProviderConfigurationCodeGenerator)codeGenerator).GenerateUseProvider("Data Source=Test"); - - Assert.Equal("UseNpgsql", result.Method); - Assert.Collection( - result.Arguments, - a => Assert.Equal("Data Source=Test", a), - a => - { - var nestedClosure = Assert.IsType(a); - - Assert.Equal("x", nestedClosure.Parameter); - Assert.Equal("UseNetTopologySuite", nestedClosure.MethodCalls[0].Method); - }); - Assert.Null(result.ChainedCall); - } - - [ConditionalFact] - public virtual void Use_provider_method_is_generated_correctly_with_NodaTime() - { - var codeGenerator = new NpgsqlCodeGenerator( - new ProviderCodeGeneratorDependencies( - [new NpgsqlNodaTimeCodeGeneratorPlugin()])); - - var result = ((IProviderConfigurationCodeGenerator)codeGenerator).GenerateUseProvider("Data Source=Test"); - - Assert.Equal("UseNpgsql", result.Method); - Assert.Collection( - result.Arguments, - a => Assert.Equal("Data Source=Test", a), - a => - { - var nestedClosure = Assert.IsType(a); - - Assert.Equal("x", nestedClosure.Parameter); - Assert.Equal("UseNodaTime", nestedClosure.MethodCalls[0].Method); - }); - Assert.Null(result.ChainedCall); - } - - private static readonly MethodInfo _setProviderOptionMethodInfo - = 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 deleted file mode 100644 index ac536bd4d4..0000000000 --- a/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs +++ /dev/null @@ -1,81 +0,0 @@ -#if DEBUG - -using Microsoft.EntityFrameworkCore.Storage.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; - -[Collection("LegacyDateTimeTest")] -public class LegacyNpgsqlTypeMappingTest : IClassFixture -{ - [Fact] - public void DateTime_type_maps_to_timestamp_by_default() - => Assert.Equal("timestamp without time zone", GetMapping(typeof(DateTime)).StoreType); - - [Fact] - public void Timestamp_maps_to_DateTime_by_default() - => Assert.Same(typeof(DateTime), GetMapping("timestamp without time zone").ClrType); - - [Fact] - public void Timestamptz_maps_to_DateTime_by_default() - => Assert.Same(typeof(DateTime), GetMapping("timestamp with time zone").ClrType); - - [Fact] - public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() - { - var mapping = GetMapping("timestamptz"); - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16Z'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Utc))); - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16Z'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Unspecified))); - - var offset = TimeZoneInfo.Local.BaseUtcOffset; - var offsetStr = (offset < TimeSpan.Zero ? '-' : '+') + offset.ToString(@"hh\:mm"); - Assert.StartsWith( - $"TIMESTAMPTZ '1997-12-17T07:37:16{offsetStr}", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Local))); - - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16.345678Z'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345, DateTimeKind.Utc).AddTicks(6780))); - } - - #region Support - - private static readonly NpgsqlTypeMappingSource Mapper = new( - new TypeMappingSourceDependencies( - new ValueConverterSelector(new ValueConverterSelectorDependencies()), - new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - [] - ), - new RelationalTypeMappingSourceDependencies([]), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSingletonOptions()); - - private static RelationalTypeMapping GetMapping(string storeType) - => Mapper.FindMapping(storeType); - - private static RelationalTypeMapping GetMapping(Type clrType) - => Mapper.FindMapping(clrType); - - public class LegacyNpgsqlTypeMappingFixture : IDisposable - { - public LegacyNpgsqlTypeMappingFixture() - { - NpgsqlTypeMappingSource.LegacyTimestampBehavior = true; - } - - public void Dispose() - => NpgsqlTypeMappingSource.LegacyTimestampBehavior = false; - } - - #endregion Support -} - -[CollectionDefinition("LegacyDateTimeTest", DisableParallelization = true)] -public class EventSourceTestCollection; - -#endif diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs deleted file mode 100644 index 65c1be9415..0000000000 --- a/test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.ValueConversion; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; - -public class NpgsqlArrayValueConverterTest -{ - private static readonly ValueConverter EnumArrayToNumberArray - = new NpgsqlArrayConverter(new EnumToNumberConverter()); - - [ConditionalFact] - public void Can_convert_enum_arrays_to_number_arrays() - { - var converter = EnumArrayToNumberArray.ConvertToProviderExpression.Compile(); - - 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)); - } - - [ConditionalFact] - public void Can_convert_enum_arrays_to_number_arrays_object() - { - var converter = EnumArrayToNumberArray.ConvertToProvider; - - 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.Null(converter(null)); - } - - [ConditionalFact] - public void Can_convert_number_arrays_to_enum_arrays() - { - var converter = EnumArrayToNumberArray.ConvertFromProviderExpression.Compile(); - - 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)); - } - - [ConditionalFact] - public void Can_convert_number_arrays_to_enum_arrays_object() - { - var converter = EnumArrayToNumberArray.ConvertFromProvider; - - 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.Null(converter(null)); - } - - private enum Beatles - { - John = 7, - Paul = 4, - George = 1, - Ringo = -1 - } -} diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs deleted file mode 100644 index 1545c1348b..0000000000 --- a/test/EFCore.PG.Tests/Storage/NpgsqlNodaTimeTypeMappingTest.cs +++ /dev/null @@ -1,867 +0,0 @@ -using System.Text; -using Microsoft.EntityFrameworkCore.Design.Internal; -using Microsoft.EntityFrameworkCore.Storage.Json; -using NodaTime; -using NodaTime.Calendars; -using NodaTime.Text; -using NodaTime.TimeZones; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; - -public class NpgsqlNodaTimeTypeMappingTest -{ - #region Timestamp without time zone - - [Fact] - public void Timestamp_maps_to_LocalDateTime_by_default() - { - Assert.Equal("timestamp without time zone", GetMapping(typeof(LocalDateTime)).StoreType); - Assert.Same(typeof(LocalDateTime), GetMapping("timestamp without time zone").ClrType); - } - - // Mapping Instant to timestamp should only be possible in legacy mode. - // However, when upgrading to 6.0 with existing 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. - [Fact] - public void Instant_maps_to_timestamp_legacy() - { - var mapping = GetMapping(typeof(Instant), "timestamp"); - Assert.Same(typeof(Instant), mapping.ClrType); - Assert.Equal("timestamp", mapping.StoreType); - } - - [Fact] - public void Instant_with_precision() - => Assert.Equal( - "timestamp(3) with time zone", - Mapper.FindMapping(typeof(Instant), "timestamp with time zone", precision: 3)!.StoreType); - - [Fact] - public void GenerateSqlLiteral_returns_LocalDateTime_literal() - { - var mapping = GetMapping(typeof(LocalDateTime)); - Assert.Equal("timestamp without time zone", mapping.StoreType); - - var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660); - Assert.Equal("TIMESTAMP '2018-04-20T10:31:33.666666'", mapping.GenerateSqlLiteral(localDateTime)); - } - - [Fact] - public void GenerateCodeLiteral_returns_LocalDateTime_literal() - { - Assert.Equal("new NodaTime.LocalDateTime(2018, 4, 20, 10, 31)", CodeLiteral(new LocalDateTime(2018, 4, 20, 10, 31))); - Assert.Equal("new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33)", CodeLiteral(new LocalDateTime(2018, 4, 20, 10, 31, 33))); - - var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33) + Period.FromNanoseconds(1); - Assert.Equal("new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33).PlusNanoseconds(1L)", CodeLiteral(localDateTime)); - } - - [Fact] - public void GenerateSqlLiteral_returns_LocalDateTime_infinity_literal() - { - var mapping = GetMapping(typeof(LocalDateTime)); - Assert.Equal(typeof(LocalDateTime), mapping.ClrType); - Assert.Equal("timestamp without time zone", mapping.StoreType); - - // TODO: Switch to use LocalDateTime.MinMaxValue when available (#4061) - Assert.Equal("TIMESTAMP '-infinity'", mapping.GenerateSqlLiteral(LocalDate.MinIsoValue + LocalTime.MinValue)); - Assert.Equal("TIMESTAMP 'infinity'", mapping.GenerateSqlLiteral(LocalDate.MaxIsoValue + LocalTime.MaxValue)); - } - - [ConditionalTheory] - [InlineData("0001-01-01T00:00:00")] - [InlineData("9999-12-31T23:59:59.9999999")] - [InlineData("2023-05-29T10:52:47.2064353")] - public void LocalDateTime_json(string dateString) - { - var readerWriter = GetMapping(typeof(LocalDateTime)).JsonValueReaderWriter!; - - var date = LocalDateTimePattern.ExtendedIso.Parse(dateString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(date)[1..^1]; - Assert.Equal(dateString, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{dateString}\"")), null); - readerManager.MoveNext(); - var actualDate = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(date, actualDate); - } - - [Fact] - public void NpgsqlRange_of_LocalDateTime_is_properly_mapped() - { - Assert.Equal("tsrange", GetMapping(typeof(NpgsqlRange)).StoreType); - Assert.Same(typeof(NpgsqlRange), GetMapping("tsrange").ClrType); - } - - [Fact] - public void GenerateSqlLiteral_returns_tsrange_literal() - { - 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), new LocalDateTime(2020, 1, 2, 12, 0, 0)); - Assert.Equal("""'["2020-01-01T12:00:00","2020-01-02T12:00:00"]'::tsrange""", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void Array_of_NpgsqlRange_of_LocalDateTime_is_properly_mapped() - { - Assert.Equal("tsmultirange", GetMapping(typeof(NpgsqlRange[])).StoreType); - Assert.Same(typeof(List>), GetMapping("tsmultirange").ClrType); - } - - [Fact] - public void List_of_NpgsqlRange_of_LocalDateTime_is_properly_mapped() - => Assert.Equal("tsmultirange", GetMapping(typeof(List>)).StoreType); - - #endregion Timestamp without time zone - - #region Timestamp with time zone - - [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_timestamptz_Instant_literal() - { - var mapping = GetMapping(typeof(Instant)); - Assert.Equal(typeof(Instant), mapping.ClrType); - Assert.Equal("timestamp with time zone", mapping.StoreType); - - var instant = (new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660)).InUtc().ToInstant(); - Assert.Equal("TIMESTAMPTZ '2018-04-20T10:31:33.666666Z'", mapping.GenerateSqlLiteral(instant)); - } - - [Fact] - public void GenerateSqlLiteral_returns_timestamptz_Instant_infinity_literal() - { - var mapping = GetMapping(typeof(Instant)); - Assert.Equal(typeof(Instant), mapping.ClrType); - Assert.Equal("timestamp with time zone", mapping.StoreType); - - Assert.Equal("TIMESTAMPTZ '-infinity'", mapping.GenerateSqlLiteral(Instant.MinValue)); - Assert.Equal("TIMESTAMPTZ 'infinity'", mapping.GenerateSqlLiteral(Instant.MaxValue)); - } - - [Fact] - public void GenerateSqlLiteral_returns_ZonedDateTime_literal() - { - var mapping = GetMapping(typeof(ZonedDateTime)); - Assert.Equal("timestamp with time zone", mapping.StoreType); - - var zonedDateTime = (new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660)) - .InZone(DateTimeZone.ForOffset(Offset.FromHours(2)), Resolvers.LenientResolver); - Assert.Equal("TIMESTAMPTZ '2018-04-20T10:31:33.666666+02'", mapping.GenerateSqlLiteral(zonedDateTime)); - } - - [Fact] - 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"))""", - CodeLiteral(zonedDateTime)); - } - - [Fact] - public void GenerateSqlLiteral_returns_OffsetDate_time_literal() - { - var mapping = GetMapping(typeof(OffsetDateTime)); - Assert.Equal("timestamp with time zone", mapping.StoreType); - - var offsetDateTime = new OffsetDateTime( - new LocalDateTime(2018, 4, 20, 10, 31, 33, 666) + Period.FromTicks(6660), - Offset.FromHours(2)); - Assert.Equal("TIMESTAMPTZ '2018-04-20T10:31:33.666666+02'", mapping.GenerateSqlLiteral(offsetDateTime)); - } - - [Fact] - public void GenerateCodeLiteral_returns_Instant_literal() - => Assert.Equal( - "NodaTime.Instant.FromUnixTimeTicks(15832607590000000L)", - CodeLiteral(Instant.FromUtc(2020, 3, 3, 18, 39, 19))); - - [Fact] - public void GenerateCodeLiteral_returns_OffsetDate_time_literal() - { - Assert.Equal( - "new NodaTime.OffsetDateTime(new NodaTime.LocalDateTime(2018, 4, 20, 10, 31), NodaTime.Offset.FromHours(-2))", - CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31), Offset.FromHours(-2)))); - - Assert.Equal( - "new NodaTime.OffsetDateTime(new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33), NodaTime.Offset.FromSeconds(9000))", - CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33), Offset.FromHoursAndMinutes(2, 30)))); - - Assert.Equal( - "new NodaTime.OffsetDateTime(new NodaTime.LocalDateTime(2018, 4, 20, 10, 31, 33), NodaTime.Offset.FromSeconds(-1))", - CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33), Offset.FromSeconds(-1)))); - } - - [ConditionalTheory] - [InlineData("0001-01-01T00:00:00Z")] - [InlineData("2023-05-29T10:52:47.2064353Z")] - [InlineData("-0005-05-05T05:55:55.555Z")] - public void Instant_json(string instantString) - { - var readerWriter = GetMapping(typeof(Instant)).JsonValueReaderWriter!; - - var date = InstantPattern.ExtendedIso.Parse(instantString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(date)[1..^1]; - Assert.Equal(instantString, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{instantString}\"")), null); - readerManager.MoveNext(); - var actualInstant = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(date, actualInstant); - } - - [ConditionalFact] - public void Instant_json_infinity() - { - var readerWriter = GetMapping(typeof(Instant)).JsonValueReaderWriter!; - - Assert.Equal("infinity", readerWriter.ToJsonString(Instant.MaxValue)[1..^1]); - Assert.Equal("-infinity", readerWriter.ToJsonString(Instant.MinValue)[1..^1]); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"infinity\""u8.ToArray()), null); - readerManager.MoveNext(); - Assert.Equal(Instant.MaxValue, readerWriter.FromJson(ref readerManager, existingObject: null)); - - readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"-infinity\""u8.ToArray()), null); - readerManager.MoveNext(); - Assert.Equal(Instant.MinValue, readerWriter.FromJson(ref readerManager, existingObject: null)); - } - - [Fact] - public void Interval_is_properly_mapped() - { - Assert.Equal("tstzrange", GetMapping(typeof(Interval)).StoreType); - Assert.Same(typeof(Interval), GetMapping("tstzrange").ClrType); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_Interval_literal() - { - var mapping = (IntervalRangeMapping)GetMapping("tstzrange"); - - var value = new Interval( - 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)); - } - - [Fact] - public void GenerateCodeLiteral_returns_tstzrange_Interval_literal() - { - Assert.Equal( - "new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), NodaTime.Instant.FromUnixTimeTicks(15782256000000000L))", - CodeLiteral( - new Interval( - new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), - new LocalDateTime(2020, 01, 05, 12, 0, 0).InUtc().ToInstant()))); - - Assert.Equal( - "new NodaTime.Interval((NodaTime.Instant?)NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), null)", - CodeLiteral(new Interval(new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), null))); - } - - [Fact] - public void Interval_array_is_properly_mapped() - { - Assert.Equal("tstzmultirange", GetMapping(typeof(Interval[])).StoreType); - Assert.Same(typeof(Interval[]), GetMapping("tstzmultirange").ClrType); - } - - [Fact] - public void Interval_list_is_properly_mapped() - => Assert.Equal("tstzmultirange", GetMapping(typeof(List)).StoreType); - - [Fact] - public void GenerateSqlLiteral_returns_Interval_array_literal() - { - var mapping = GetMapping(typeof(Interval[])); - - var interval = new Interval[] - { - new( - new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), - new( - new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), - }; - - Assert.Equal( - "'{[1998-04-12T13:26:38Z,1998-04-12T15:26:38Z), [1998-04-13T13:26:38Z,1998-04-13T15:26:38Z)}'::tstzmultirange", - mapping.GenerateSqlLiteral(interval)); - } - - [Fact] - public void GenerateSqlLiteral_returns_Interval_list_literal() - { - var mapping = GetMapping(typeof(List)); - - var interval = new List - { - new( - new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), - new( - new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), - }; - - Assert.Equal( - "'{[1998-04-12T13:26:38Z,1998-04-12T15:26:38Z), [1998-04-13T13:26:38Z,1998-04-13T15:26:38Z)}'::tstzmultirange", - mapping.GenerateSqlLiteral(interval)); - } - - [Fact] - public void GenerateCodeLiteral_returns_Interval_array_literal() - => Assert.Equal( - "new[] { new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8923875980000000L), NodaTime.Instant.FromUnixTimeTicks(8923947980000000L)), new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8924739980000000L), NodaTime.Instant.FromUnixTimeTicks(8924811980000000L)) }", - CodeLiteral( - new Interval[] - { - new( - new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), - new( - new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), - })); - - [Fact] - public void GenerateCodeLiteral_returns_Interval_list_literal() - => Assert.Equal( - "new List { new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8923875980000000L), NodaTime.Instant.FromUnixTimeTicks(8923947980000000L)), new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8924739980000000L), NodaTime.Instant.FromUnixTimeTicks(8924811980000000L)) }", - CodeLiteral( - new List - { - new( - new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), - new( - new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), - new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), - })); - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_Instant_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tstzrange", mapping.StoreType); - Assert.Equal("timestamp with 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"]'::tstzrange""", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_ZonedDateTime_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tstzrange", mapping.StoreType); - Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); - - 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)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_OffsetDateTime_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tstzrange", mapping.StoreType); - Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); - - 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)); - } - - [Fact] - public void Array_of_NpgsqlRange_of_Instant_is_properly_mapped() - => Assert.Equal("tstzmultirange", GetMapping(typeof(NpgsqlRange[])).StoreType); - - [Fact] - public void List_of_NpgsqlRange_of_Instant_is_properly_mapped() - => Assert.Equal("tstzmultirange", GetMapping(typeof(List>)).StoreType); - - #endregion Timestamp with time zone - - #region date/daterange/datemultirange - - [Fact] - public void LocalDate_is_properly_mapped() - { - Assert.Equal("date", GetMapping(typeof(LocalDate)).StoreType); - Assert.Same(typeof(LocalDate), GetMapping("date").ClrType); - } - - [Fact] - public void GenerateSqlLiteral_returns_LocalDate_literal() - { - var mapping = GetMapping(typeof(LocalDate)); - - Assert.Equal("DATE '2018-04-20'", mapping.GenerateSqlLiteral(new LocalDate(2018, 4, 20))); - } - - [Fact] - public void GenerateSqlLiteral_returns_LocalDate_infinity_literal() - { - var mapping = GetMapping(typeof(LocalDate)); - - Assert.Equal("DATE '-infinity'", mapping.GenerateSqlLiteral(LocalDate.MinIsoValue)); - Assert.Equal("DATE 'infinity'", mapping.GenerateSqlLiteral(LocalDate.MaxIsoValue)); - } - - [Fact] - public void GenerateCodeLiteral_returns_LocalDate_literal() - { - Assert.Equal("new NodaTime.LocalDate(2018, 4, 20)", CodeLiteral(new LocalDate(2018, 4, 20))); - Assert.Equal("new NodaTime.LocalDate(-2017, 4, 20)", CodeLiteral(new LocalDate(Era.BeforeCommon, 2018, 4, 20))); - } - - [ConditionalTheory] - [InlineData("0001-01-01")] - [InlineData("2023-05-29")] - [InlineData("-0005-05-05")] - public void LocalDate_json(string dateString) - { - var readerWriter = GetMapping(typeof(LocalDate)).JsonValueReaderWriter!; - - var date = LocalDatePattern.Iso.Parse(dateString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(date)[1..^1]; - Assert.Equal(dateString, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{dateString}\"")), null); - readerManager.MoveNext(); - var actualDate = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(date, actualDate); - } - - [ConditionalFact] - public void LocalDate_json_infinity() - { - var readerWriter = GetMapping(typeof(LocalDate)).JsonValueReaderWriter!; - - Assert.Equal("infinity", readerWriter.ToJsonString(LocalDate.MaxIsoValue)[1..^1]); - Assert.Equal("-infinity", readerWriter.ToJsonString(LocalDate.MinIsoValue)[1..^1]); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"infinity\""u8.ToArray()), null); - readerManager.MoveNext(); - Assert.Equal(LocalDate.MaxIsoValue, readerWriter.FromJson(ref readerManager, existingObject: null)); - - readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"-infinity\""u8.ToArray()), null); - readerManager.MoveNext(); - Assert.Equal(LocalDate.MinIsoValue, readerWriter.FromJson(ref readerManager, existingObject: null)); - } - - [Fact] - public void DateInterval_is_properly_mapped() - { - Assert.Equal("daterange", GetMapping(typeof(DateInterval)).StoreType); - Assert.Same(typeof(DateInterval), GetMapping("daterange").ClrType); - } - - [Fact] - public void GenerateSqlLiteral_returns_DateInterval_literal() - { - var mapping = GetMapping(typeof(DateInterval)); - Assert.Equal("daterange", mapping.StoreType); - - var interval = new DateInterval(new LocalDate(2020, 01, 01), new LocalDate(2020, 12, 25)); - Assert.Equal("'[2020-01-01,2020-12-25]'::daterange", mapping.GenerateSqlLiteral(interval)); - } - - [Fact] - public void GenerateCodeLiteral_returns_DateInterval_literal() - => Assert.Equal( - "new NodaTime.DateInterval(new NodaTime.LocalDate(2020, 1, 1), new NodaTime.LocalDate(2020, 12, 25))", - CodeLiteral(new DateInterval(new LocalDate(2020, 01, 01), new LocalDate(2020, 12, 25)))); - - [Fact] - public void DateInterval_array_is_properly_mapped() - { - Assert.Equal("datemultirange", GetMapping(typeof(DateInterval[])).StoreType); - Assert.Same(typeof(DateInterval[]), GetMapping("datemultirange").ClrType); - } - - [Fact] - public void DateInterval_list_is_properly_mapped() - => Assert.Equal("datemultirange", GetMapping(typeof(List)).StoreType); - - [Fact] - public void GenerateSqlLiteral_returns_DateInterval_array_literal() - { - var mapping = GetMapping(typeof(DateInterval[])); - - var interval = new DateInterval[] - { - new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) - }; - - Assert.Equal("'{[2002-03-04,2002-03-05], [2002-03-08,2002-03-10]}'::datemultirange", mapping.GenerateSqlLiteral(interval)); - } - - [Fact] - public void GenerateSqlLiteral_returns_DateInterval_list_literal() - { - var mapping = GetMapping(typeof(List)); - - var interval = new List - { - new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) - }; - - Assert.Equal("'{[2002-03-04,2002-03-05], [2002-03-08,2002-03-10]}'::datemultirange", mapping.GenerateSqlLiteral(interval)); - } - - [Fact] - public void GenerateCodeLiteral_returns_DateInterval_array_literal() - => Assert.Equal( - "new[] { new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 4), new NodaTime.LocalDate(2002, 3, 5)), new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 8), new NodaTime.LocalDate(2002, 3, 10)) }", - CodeLiteral( - new DateInterval[] - { - new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), - new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) - })); - - [Fact] - public void GenerateCodeLiteral_returns_DateInterval_list_literal() - => Assert.Equal( - "new List { new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 4), new NodaTime.LocalDate(2002, 3, 5)), new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 8), new NodaTime.LocalDate(2002, 3, 10)) }", - CodeLiteral( - new List - { - new(new LocalDate(2002, 3, 4), new LocalDate(2002, 3, 5)), - new(new LocalDate(2002, 3, 8), new LocalDate(2002, 3, 10)) - })); - - [Fact] - public void NpgsqlRange_of_LocalDate_is_properly_mapped() - => Assert.Equal("daterange", GetMapping(typeof(NpgsqlRange)).StoreType); - - [Fact] - public void GenerateSqlLiteral_returns_daterange_LocalDate_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - var value = new NpgsqlRange(new LocalDate(2020, 1, 1), new LocalDate(2020, 1, 2)); - Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void Array_of_NpgsqlRange_of_LocalDate_is_properly_mapped() - => Assert.Equal("datemultirange", GetMapping(typeof(NpgsqlRange[])).StoreType); - - [Fact] - public void List_of_NpgsqlRange_of_LocalDate_is_properly_mapped() - => Assert.Equal("datemultirange", GetMapping(typeof(List>)).StoreType); - - #endregion date/daterange/datemultirange - - #region time - - [Fact] - public void GenerateSqlLiteral_returns_LocalTime_literal() - { - var mapping = GetMapping(typeof(LocalTime)); - - Assert.Equal("TIME '10:31:33'", mapping.GenerateSqlLiteral(new LocalTime(10, 31, 33))); - Assert.Equal("TIME '10:31:33.666'", mapping.GenerateSqlLiteral(new LocalTime(10, 31, 33, 666))); - Assert.Equal("TIME '10:31:33.666666'", mapping.GenerateSqlLiteral(new LocalTime(10, 31, 33, 666) + Period.FromTicks(6660))); - } - - [Fact] - public void GenerateCodeLiteral_returns_LocalTime_literal() - { - Assert.Equal("new NodaTime.LocalTime(9, 30)", CodeLiteral(new LocalTime(9, 30))); - Assert.Equal("new NodaTime.LocalTime(9, 30, 15)", CodeLiteral(new LocalTime(9, 30, 15))); - Assert.Equal( - "NodaTime.LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 500000000L)", CodeLiteral(new LocalTime(9, 30, 15, 500))); - Assert.Equal( - "NodaTime.LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 1L)", - CodeLiteral(LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 1))); - } - - [Fact] - public void LocalTime_array_is_properly_mapped() - { - Assert.Equal("time[]", GetMapping(typeof(LocalTime[])).StoreType); - Assert.Same(typeof(List), GetMapping("time[]").ClrType); - } - - [Fact] - public void LocalTime_list_is_properly_mapped() - => Assert.Equal("time[]", GetMapping(typeof(List)).StoreType); - - [ConditionalTheory] - [InlineData("00:00:00.0000000", "00:00:00")] - [InlineData("23:59:59.9999999", "23:59:59.9999999")] - [InlineData("11:05:12.3456789", "11:05:12.3456789")] - public void LocalTime_json(string timeString, string json) - { - var readerWriter = GetMapping(typeof(LocalTime)).JsonValueReaderWriter!; - - var time = LocalTimePattern.ExtendedIso.Parse(timeString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(time)[1..^1]; - Assert.Equal(json, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); - readerManager.MoveNext(); - var actualTime = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(time, actualTime); - } - - #endregion time - - #region timetz - - [Fact] - public void GenerateSqlLiteral_returns_OffsetTime_literal() - { - var mapping = GetMapping(typeof(OffsetTime)); - - Assert.Equal( - "TIMETZ '10:31:33+02'", mapping.GenerateSqlLiteral( - new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHours(2)))); - Assert.Equal( - "TIMETZ '10:31:33-02:30'", mapping.GenerateSqlLiteral( - new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHoursAndMinutes(-2, -30)))); - Assert.Equal( - "TIMETZ '10:31:33.666666Z'", mapping.GenerateSqlLiteral( - new OffsetTime(new LocalTime(10, 31, 33, 666) + Period.FromTicks(6660), Offset.Zero))); - } - - [Fact] - public void GenerateCodeLiteral_returns_OffsetTime_literal() - => Assert.Equal( - "new NodaTime.OffsetTime(new NodaTime.LocalTime(10, 31, 33), NodaTime.Offset.FromHours(2))", - CodeLiteral(new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHours(2)))); - - [ConditionalTheory] - [InlineData("00:00:00.0000000Z", "00:00:00Z")] - [InlineData("23:59:59.999999Z", "23:59:59.999999Z")] - [InlineData("11:05:12-02", "11:05:12-02")] - public void OffsetTime_json(string timeString, string json) - { - var readerWriter = GetMapping(typeof(OffsetTime)).JsonValueReaderWriter!; - - var timeOffset = OffsetTimePattern.ExtendedIso.Parse(timeString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(timeOffset)[1..^1]; - Assert.Equal(json, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); - readerManager.MoveNext(); - var actualTimeOffset = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(timeOffset, actualTimeOffset); - } - - #endregion timetz - - #region interval - - [Fact] - public void Duration_is_properly_mapped() - => Assert.All( - [GetMapping(typeof(Duration)), GetMapping(typeof(Duration), "interval")], - m => - { - Assert.Equal("interval", m.StoreType); - Assert.Same(typeof(Duration), m.ClrType); - }); - - [Fact] - public void Period_is_properly_mapped() - => Assert.All( - [GetMapping(typeof(Period)), GetMapping(typeof(Period), "interval")], - m => - { - Assert.Equal("interval", m.StoreType); - Assert.Same(typeof(Period), m.ClrType); - }); - - [Fact] - public void GenerateSqlLiteral_returns_Period_literal() - { - var mapping = GetMapping(typeof(Period)); - - var hms = Period.FromHours(4) + Period.FromMinutes(3) + Period.FromSeconds(2); - Assert.Equal("INTERVAL 'PT4H3M2S'", mapping.GenerateSqlLiteral(hms)); - - var withMilliseconds = hms + Period.FromMilliseconds(1); - Assert.Equal("INTERVAL 'PT4H3M2.001S'", mapping.GenerateSqlLiteral(withMilliseconds)); - - var withMicroseconds = hms + Period.FromTicks(6660); - Assert.Equal("INTERVAL 'PT4H3M2.000666S'", mapping.GenerateSqlLiteral(withMicroseconds)); - - var withYearMonthDay = hms + Period.FromYears(2018) + Period.FromMonths(4) + Period.FromDays(20); - Assert.Equal("INTERVAL 'P2018Y4M20DT4H3M2S'", mapping.GenerateSqlLiteral(withYearMonthDay)); - } - - [Fact] - public void GenerateCodeLiteral_returns_Period_literal() - { - Assert.Equal("NodaTime.Period.FromHours(5L)", CodeLiteral(Period.FromHours(5))); - - Assert.Equal( - "NodaTime.Period.FromYears(1) + NodaTime.Period.FromMonths(2) + NodaTime.Period.FromWeeks(3) + " - + "NodaTime.Period.FromDays(4) + NodaTime.Period.FromHours(5L) + NodaTime.Period.FromMinutes(6L) + " - + "NodaTime.Period.FromSeconds(7L) + NodaTime.Period.FromMilliseconds(8L) + NodaTime.Period.FromNanoseconds(9L)", - CodeLiteral( - Period.FromYears(1) - + Period.FromMonths(2) - + Period.FromWeeks(3) - + Period.FromDays(4) - + Period.FromHours(5) - + Period.FromMinutes(6) - + Period.FromSeconds(7) - + Period.FromMilliseconds(8) - + Period.FromNanoseconds(9))); - - Assert.Equal("NodaTime.Period.Zero", CodeLiteral(Period.Zero)); - } - - [Fact] - public void GenerateCodeLiteral_returns_Duration_literal() - { - Assert.Equal("NodaTime.Duration.FromHours(5)", CodeLiteral(Duration.FromHours(5))); - - Assert.Equal( - "NodaTime.Duration.FromDays(4) + NodaTime.Duration.FromHours(5) + NodaTime.Duration.FromMinutes(6L) + " - + "NodaTime.Duration.FromSeconds(7L) + NodaTime.Duration.FromMilliseconds(8L)", - CodeLiteral( - Duration.FromDays(4) - + Duration.FromHours(5) - + Duration.FromMinutes(6) - + Duration.FromSeconds(7) - + Duration.FromMilliseconds(8))); - - Assert.Equal("NodaTime.Duration.Zero", CodeLiteral(Duration.Zero)); - } - - [ConditionalTheory] - [InlineData("-10675199:02:48:05.477580", "-10675199 02:48:05.47758")] - [InlineData("10675199:02:48:05.477580", "10675199 02:48:05.47758")] - [InlineData("00:00:00", "00:00:00")] - [InlineData("12:23:23.801885", "12:23:23.801885")] - public void Duration_json(string durationString, string json) - { - var readerWriter = GetMapping(typeof(Duration)).JsonValueReaderWriter!; - - var duration = durationString.Count(c => c == ':') == 3 // there's a Days component - ? DurationPattern.Roundtrip.Parse(durationString).GetValueOrThrow() - : DurationPattern.JsonRoundtrip.Parse(durationString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(duration)[1..^1]; - Assert.Equal(json, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); - readerManager.MoveNext(); - var actualDuration = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(duration, actualDuration); - } - - [ConditionalTheory] - [InlineData("P2018Y4M20DT4H3M2S")] - public void Period_json(string intervalString) - { - var readerWriter = GetMapping(typeof(Period)).JsonValueReaderWriter!; - - var period = PeriodPattern.NormalizingIso.Parse(intervalString).GetValueOrThrow(); - var actualJson = readerWriter.ToJsonString(period)[1..^1]; - Assert.Equal(intervalString, actualJson); - - // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{intervalString}\"")), null); - readerManager.MoveNext(); - var actualPeriod = readerWriter.FromJson(ref readerManager, existingObject: null); - Assert.Equal(period, actualPeriod); - } - - - #endregion interval - - #region DateTimeZone - - [Fact] - public void DateTimeZone_is_properly_mapped() - { - var mapping = GetMapping(typeof(DateTimeZone)); - - Assert.Same(typeof(DateTimeZone), mapping.ClrType); - Assert.Equal("text", mapping.StoreType); - } - - [Fact] - public void GenerateSqlLiteral_returns_DateTimeZone_literal() - { - var mapping = GetMapping(typeof(DateTimeZone)); - - Assert.Equal("Europe/Berlin", mapping.GenerateSqlLiteral(DateTimeZoneProviders.Tzdb["Europe/Berlin"])); - } - - [Fact] - public void GenerateCodeLiteral_returns_DateTimezone_literal() - => Assert.Equal( - """NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("Europe/Berlin")""", - CodeLiteral(DateTimeZoneProviders.Tzdb["Europe/Berlin"])); - - #endregion - - #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 static readonly CSharpHelper CsHelper = new(Mapper); - - private static string CodeLiteral(object value) - => CsHelper.UnknownLiteral(value); - - #endregion Support -} diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlSqlGenerationHelperTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlSqlGenerationHelperTest.cs deleted file mode 100644 index b6985fe0c4..0000000000 --- a/test/EFCore.PG.Tests/Storage/NpgsqlSqlGenerationHelperTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; - -public class NpgsqlSqlGenerationHelperTest -{ - [Theory] - [InlineData("all_lowercase", null, "all_lowercase", "all_lowercase")] - [InlineData("digit", null, "digit", "digit")] - [InlineData("under_score", null, "under_score", "under_score")] - [InlineData("dollar$", null, "dollar$", "dollar$")] - [InlineData("ALL_CAPS", null, "\"ALL_CAPS\"", "\"ALL_CAPS\"")] - [InlineData("oneCap", null, "\"oneCap\"", "\"oneCap\"")] - [InlineData("0starts_with_digit", null, "\"0starts_with_digit\"", "\"0starts_with_digit\"")] - [InlineData("all_lowercase", "all_lowercase", "all_lowercase.all_lowercase", "all_lowercase")] - [InlineData("all_lowercase", "oneCap", "\"oneCap\".all_lowercase", "all_lowercase")] - [InlineData("oneCap", "all_lowercase", "all_lowercase.\"oneCap\"", "\"oneCap\"")] - [InlineData("CAPS", "CAPS", "\"CAPS\".\"CAPS\"", "\"CAPS\"")] - [InlineData("select", "null", "\"null\".\"select\"", "\"select\"")] - public virtual void DelimitIdentifier_quotes_properly( - string identifier, - string schema, - string outputWithSchema, - string outputWithoutSchema) - { - var helper = CreateSqlGenerationHelper(); - Assert.Equal(outputWithoutSchema, helper.DelimitIdentifier(identifier)); - Assert.Equal(outputWithSchema, helper.DelimitIdentifier(identifier, schema)); - - var sb = new StringBuilder(); - helper.DelimitIdentifier(sb, identifier); - Assert.Equal(outputWithoutSchema, sb.ToString()); - - sb = new StringBuilder(); - helper.DelimitIdentifier(sb, identifier, schema); - Assert.Equal(outputWithSchema, sb.ToString()); - } - - private ISqlGenerationHelper CreateSqlGenerationHelper() - => new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()); -} diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs deleted file mode 100644 index d31012889d..0000000000 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs +++ /dev/null @@ -1,460 +0,0 @@ -using System.Net; -using System.Net.NetworkInformation; -using Microsoft.EntityFrameworkCore.Storage.Json; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; - -public class NpgsqlTypeMappingSourceTest -{ - [Theory] - [InlineData("integer", typeof(int), null, null, null, false)] - [InlineData("integer[]", typeof(List), null, null, null, false)] - [InlineData("int", typeof(int), null, null, null, false)] - [InlineData("int[]", typeof(List), null, null, null, false)] - [InlineData("numeric", typeof(decimal), null, null, null, false)] - [InlineData("numeric(10,2)", typeof(decimal), null, 10, 2, false)] - [InlineData("text", typeof(string), null, null, null, false)] - [InlineData("TEXT", typeof(string), null, null, null, false)] - [InlineData("character(8)", typeof(string), 8, null, null, true)] - [InlineData("char(8)", typeof(string), 8, null, null, true)] - [InlineData("character(1)", typeof(char), 1, null, null, true)] - [InlineData("char(1)", typeof(char), 1, null, null, true)] - [InlineData("character", typeof(char), null, null, null, true)] - [InlineData("character varying(8)", typeof(string), 8, null, null, false)] - [InlineData("varchar(8)", typeof(string), 8, null, null, false)] - [InlineData("varchar", typeof(string), null, null, null, false)] - [InlineData("timestamp with time zone", typeof(DateTime), null, null, null, false)] - [InlineData("timestamp without time zone", typeof(DateTime), null, null, null, false)] - [InlineData("date", typeof(DateOnly), null, null, null, false)] - [InlineData("time", typeof(TimeOnly), null, null, null, false)] - [InlineData("time without time zone", typeof(TimeOnly), null, null, null, false)] - [InlineData("interval", typeof(TimeSpan), null, null, null, false)] - [InlineData("dummy", typeof(DummyType), null, null, null, false)] - [InlineData("int4range", typeof(NpgsqlRange), null, null, null, false)] - [InlineData("floatrange", typeof(NpgsqlRange), null, null, null, false)] - [InlineData("dummyrange", typeof(NpgsqlRange), null, null, null, false)] - [InlineData("int4multirange", typeof(List>), null, null, null, false)] - [InlineData("geometry", typeof(Geometry), null, null, null, false)] - [InlineData("geometry(Polygon)", typeof(Polygon), null, null, null, false)] - [InlineData("geography(Point, 4326)", typeof(Point), null, null, null, false)] - [InlineData("geometry(pointz, 4326)", typeof(Point), null, null, null, false)] - [InlineData("geography(LineStringZM)", typeof(LineString), null, null, null, false)] - [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)] - [InlineData("cidr", typeof(IPNetwork), 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); - - Assert.NotNull(mapping); - Assert.Same(type, mapping.ClrType); - Assert.Equal(size, mapping.Size); - Assert.Equal(precision, mapping.Precision); - Assert.Equal(scale, mapping.Scale); - Assert.False(mapping.IsUnicode); - Assert.Equal(fixedLength, mapping.IsFixedLength); - Assert.Equal(typeName, mapping.StoreType); - } - - [Fact] - public void Varchar32() - { - var mapping = CreateTypeMappingSource().FindMapping("varchar(32)"); - Assert.Same(typeof(string), mapping.ClrType); - Assert.Equal("varchar(32)", mapping.StoreType); - Assert.Equal(32, mapping.Size); - } - - [Fact] - public void Varchar32_Array() - { - var mapping = CreateTypeMappingSource().FindMapping("varchar(32)[]"); - - var arrayMapping = Assert.IsAssignableFrom(mapping); - Assert.Same(typeof(List), arrayMapping.ClrType); - Assert.Equal("varchar(32)[]", arrayMapping.StoreType); - Assert.Null(arrayMapping.Size); - - var elementMapping = arrayMapping.ElementTypeMapping; - Assert.Same(typeof(string), elementMapping.ClrType); - Assert.Equal("varchar(32)", elementMapping.StoreType); - Assert.Equal(32, elementMapping.Size); - } - - [Fact] - public void Timestamp_without_time_zone_5() - { - var mapping = CreateTypeMappingSource().FindMapping("timestamp(5) without time zone"); - Assert.Same(typeof(DateTime), mapping.ClrType); - Assert.Equal("timestamp(5) without time zone", mapping.StoreType); - // Precision/Scale not actually exposed on RelationalTypeMapping... - } - - [Fact] - public void Timestamp_without_time_zone_Array_5() - { - var arrayMapping = - Assert.IsAssignableFrom(CreateTypeMappingSource().FindMapping("timestamp(5) without time zone[]")); - Assert.Same(typeof(List), arrayMapping.ClrType); - Assert.Equal("timestamp(5) without time zone[]", arrayMapping.StoreType); - - var elementMapping = arrayMapping.ElementTypeMapping; - Assert.Same(typeof(DateTime), elementMapping.ClrType); - Assert.Equal("timestamp(5) without time zone", elementMapping.StoreType); - } - - [Theory] - [InlineData(typeof(int), "integer")] - [InlineData(typeof(int[]), "integer[]")] - [InlineData(typeof(byte[]), "bytea")] - [InlineData(typeof(DateTime), "timestamp with time zone")] - [InlineData(typeof(DateOnly), "date")] - [InlineData(typeof(TimeOnly), "time without time zone")] - [InlineData(typeof(TimeSpan), "interval")] - [InlineData(typeof(DummyType), "dummy")] - [InlineData(typeof(NpgsqlRange), "int4range")] - [InlineData(typeof(NpgsqlRange), "floatrange")] - [InlineData(typeof(NpgsqlRange), "dummyrange")] - [InlineData(typeof(NpgsqlRange[]), "int4multirange")] - [InlineData(typeof(List>), "int4multirange")] - [InlineData(typeof(Geometry), "geometry")] - [InlineData(typeof(Point), "geometry")] - [InlineData(typeof(IPAddress), "inet")] - [InlineData(typeof(IPNetwork), "cidr")] -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - [InlineData(typeof(NpgsqlCidr), "cidr")] // legacy -#pragma warning restore CS0618 - [InlineData(typeof(PhysicalAddress), "macaddr")] - public void By_ClrType(Type clrType, string expectedStoreType) - { - var mapping = CreateTypeMappingSource().FindMapping(clrType); - Assert.Equal(expectedStoreType, mapping.StoreType); - Assert.Same(clrType, mapping.ClrType); - } - - [Theory] - [InlineData(typeof(decimal), "numeric(5)")] - [InlineData(typeof(DateTime), "timestamp(5) with time zone")] - [InlineData(typeof(TimeSpan), "interval(5)")] - [InlineData(typeof(int), "integer")] - public void By_ClrType_and_precision(Type clrType, string expectedStoreType) - { - var mapping = CreateTypeMappingSource().FindMapping(clrType, null, precision: 5); - Assert.Equal(expectedStoreType, mapping.StoreType); - Assert.Same(clrType, mapping.ClrType); - } - - [Theory] - [InlineData(typeof(decimal[]), "numeric(5)[]")] - [InlineData(typeof(DateTime[]), "timestamp(5) with time zone[]")] - [InlineData(typeof(TimeSpan[]), "interval(5)[]")] - [InlineData(typeof(int[]), "integer[]")] - public void By_ClrType_and_element_precision(Type clrType, string expectedStoreType) - { - var model = CreateEmptyModel(); - var arrayMapping = CreateTypeMappingSource().FindMapping( - clrType, model, - CreateTypeMappingSource().FindMapping(clrType.GetElementType()!, null, precision: 5)!); - - Assert.Equal(expectedStoreType, arrayMapping.StoreType); - Assert.Same(clrType, arrayMapping.ClrType); - Assert.Null(arrayMapping.Precision); - - var elementMapping = Assert.IsAssignableFrom(arrayMapping.ElementTypeMapping); - Assert.Equal(5, elementMapping.Precision); - Assert.Equal(expectedStoreType[..^2], elementMapping.StoreType); - Assert.Same(clrType.GetElementType(), elementMapping.ClrType); - } - - [Theory] - [InlineData("integer", typeof(int))] - [InlineData("numeric", typeof(float))] - [InlineData("numeric", typeof(double))] - [InlineData("date", typeof(DateOnly))] - [InlineData("date", typeof(DateTime))] - [InlineData("time", typeof(TimeOnly))] - [InlineData("time", typeof(TimeSpan))] - [InlineData("integer[]", typeof(int[]))] - [InlineData("integer[]", typeof(List))] - [InlineData("smallint[]", typeof(byte[]))] - [InlineData("dummy", typeof(DummyType))] - [InlineData("int4range", typeof(NpgsqlRange))] - [InlineData("floatrange", typeof(NpgsqlRange))] - [InlineData("dummyrange", typeof(NpgsqlRange))] - [InlineData("geometry", typeof(Geometry))] - [InlineData("geometry(Point, 4326)", typeof(Geometry))] - [InlineData("xid", typeof(uint))] - [InlineData("xid8", typeof(ulong))] - public void By_StoreType_with_ClrType(string storeType, Type clrType) - { - var mapping = CreateTypeMappingSource().FindMapping(clrType, storeType); - Assert.Equal(storeType, mapping.StoreType); - Assert.Same(clrType, mapping.ClrType); - } - - [Theory] - [InlineData("integer", typeof(UnknownType))] - //[InlineData("integer[]", typeof(UnknownType))] TODO Implement - [InlineData("dummy", typeof(UnknownType))] - [InlineData("int4range", typeof(UnknownType))] - [InlineData("floatrange", typeof(UnknownType))] - [InlineData("dummyrange", typeof(UnknownType))] - [InlineData("geometry", typeof(UnknownType))] - public void By_StoreType_with_wrong_ClrType(string storeType, Type wrongClrType) - => Assert.Null(CreateTypeMappingSource().FindMapping(wrongClrType, storeType)); - - // Happens when using domain/aliases: we don't know about the domain but continue with the mapping based on the ClrType - [Fact] - public void Unknown_StoreType_with_known_ClrType() - => Assert.Equal("some_domain", CreateTypeMappingSource().FindMapping(typeof(int), "some_domain").StoreType); - - [Fact] - public void Varchar_mapping_sets_NpgsqlDbType() - { - var mapping = CreateTypeMappingSource().FindMapping("character varying"); - var parameter = (NpgsqlParameter)mapping.CreateParameter(new NpgsqlCommand(), "p", "foo"); - Assert.Equal(NpgsqlDbType.Varchar, parameter.NpgsqlDbType); - } - - [Fact] - public void Single_char_mapping_sets_NpgsqlDbType() - { - var mapping = CreateTypeMappingSource().FindMapping(typeof(char)); - var parameter = (NpgsqlParameter)mapping.CreateParameter(new NpgsqlCommand(), "p", "foo"); - Assert.Equal(NpgsqlDbType.Char, parameter.NpgsqlDbType); - } - - [Fact] - public void String_as_single_char_mapping_sets_NpgsqlDbType() - { - var mapping = CreateTypeMappingSource().FindMapping(typeof(string), "char(1)"); - var parameter = (NpgsqlParameter)mapping.CreateParameter(new NpgsqlCommand(), "p", "foo"); - Assert.Equal(NpgsqlDbType.Char, parameter.NpgsqlDbType); - } - - #region Array - - [Fact] - public void Primitive_collection() - { - var mapping = CreateTypeMappingSource().FindMapping(typeof(int[])); - Assert.IsType(mapping, exactMatch: false); - Assert.Equal("integer[]", mapping.StoreType); - Assert.Same(typeof(int[]), mapping.ClrType); - } - - [Fact] - public void Array_over_type_mapping_with_value_converter_by_clr_type_array() - => Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping(typeof(LTree[])), typeof(LTree[])); - - [Fact] - public void Array_over_type_mapping_with_value_converter_by_clr_type_list() - => Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping(typeof(List)), typeof(List)); - - [Fact] - public void Array_over_type_mapping_with_value_converter_by_store_type() - => Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping("ltree[]"), typeof(List)); - - private void Array_over_type_mapping_with_value_converter(CoreTypeMapping mapping, Type expectedType) - { - var arrayMapping = (NpgsqlArrayTypeMapping)mapping; - Assert.Equal("ltree[]", arrayMapping.StoreType); - Assert.Same(expectedType, arrayMapping.ClrType); - - var elementMapping = arrayMapping.ElementTypeMapping; - Assert.NotNull(elementMapping); - Assert.Equal("ltree", elementMapping.StoreType); - Assert.Same(typeof(LTree), elementMapping.ClrType); - - var arrayConverter = arrayMapping.Converter; - Assert.NotNull(arrayConverter); - Assert.Same(expectedType, arrayConverter.ModelClrType); - Assert.Same(typeof(string[]), arrayConverter.ProviderClrType); - - Assert.Collection( - (ICollection)arrayConverter.ConvertToProvider( - expectedType.IsArray - ? new LTree[] { new("foo"), new("bar") } - : new List { new("foo"), new("bar") }), - s => Assert.Equal("foo", s), - s => Assert.Equal("bar", s)); - } - - #endregion Array - - #region JSON - - [Fact] - public void Json_structural() - { - var mapping = CreateTypeMappingSource().FindMapping(typeof(JsonTypePlaceholder)); - Assert.Equal("jsonb", mapping.StoreType); - Assert.Same(typeof(JsonTypePlaceholder), mapping.ClrType); - } - - [Fact] - public void Json_primitive_collection() - { - var mapping = CreateTypeMappingSource().FindMapping(typeof(int[]), "jsonb"); - Assert.Equal("jsonb", mapping.StoreType); - Assert.Same(typeof(IEnumerable), mapping.ClrType); - - var elementMapping = (RelationalTypeMapping)mapping.ElementTypeMapping; - Assert.NotNull(elementMapping); - Assert.Equal("integer", elementMapping.StoreType); - Assert.Same(typeof(int), elementMapping.ClrType); - } - - #endregion JSON - - #region Multirange - - [Fact] - public void Multirange_by_clr_type_across_pg_versions() - { - var mapping14 = CreateTypeMappingSource(postgresVersion: new Version(14, 0)).FindMapping(typeof(NpgsqlRange[]))!; - var mapping13 = CreateTypeMappingSource(postgresVersion: new Version(13, 0)).FindMapping(typeof(NpgsqlRange[]))!; - var mappingDefault = CreateTypeMappingSource().FindMapping(typeof(NpgsqlRange[]))!; - - Assert.Equal("int4multirange", mapping14.StoreType); - Assert.Equal("int4range[]", mapping13.StoreType); - - // See #2351 - we didn't put multiranges behind a version opt-in in 6.0, although the default PG version is still 12; this causes - // anyone with arrays of ranges to fail if they upgrade to 6.0 with pre-14 PG. - // Changing this in a patch would break people already using 6.0 with PG14, so multiranges are on by default unless users explicitly - // specify < 14. - // Once 14 is made the default version, this stuff can be removed. - Assert.Equal("int4multirange", mappingDefault.StoreType); - } - - [Fact] - public void Multirange_by_store_type_across_pg_versions() - { - var mapping14 = CreateTypeMappingSource(postgresVersion: new Version(14, 0)).FindMapping("int4multirange")!; - var mapping13 = CreateTypeMappingSource(postgresVersion: new Version(13, 0)).FindMapping("int4multirange"); - var mappingDefault = CreateTypeMappingSource().FindMapping("int4multirange")!; - - Assert.Same(typeof(List>), mapping14.ClrType); - Assert.Null(mapping13); - - // See #2351 - we didn't put multiranges behind a version opt-in in 6.0, although the default PG version is still 12; this causes - // anyone with arrays of ranges to fail if they upgrade to 6.0 with pre-14 PG. - // Changing this in a patch would break people already using 6.0 with PG14, so multiranges are on by default unless users explicitly - // specify < 14. - // Once 14 is made the default version, this stuff can be removed. - Assert.Same(typeof(List>), mappingDefault.ClrType); - } - - #endregion Multirange - -#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) - { - var builder = new DbContextOptionsBuilder(); - var npgsqlBuilder = new NpgsqlDbContextOptionsBuilder(builder); - - npgsqlBuilder.MapRange("floatrange"); - npgsqlBuilder.MapRange("dummyrange", subtypeName: "dummy"); - npgsqlBuilder.SetPostgresVersion(postgresVersion); - - var options = new NpgsqlSingletonOptions(); - options.Initialize(builder.Options); - - return new NpgsqlTypeMappingSource( - new TypeMappingSourceDependencies( - new ValueConverterSelector(new ValueConverterSelectorDependencies()), - new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - []), - new RelationalTypeMappingSourceDependencies( - [ - new NpgsqlNetTopologySuiteTypeMappingSourcePlugin(new NpgsqlNetTopologySuiteSingletonOptions()), - new DummyTypeMappingSourcePlugin() - ]), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - options); - } - - private class DummyTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin - { - public RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) - => mappingInfo.StoreTypeName is not null - ? mappingInfo.StoreTypeName == "dummy" && (mappingInfo.ClrType is null || mappingInfo.ClrType == typeof(DummyType)) - ? _dummyMapping - : null - : mappingInfo.ClrType == typeof(DummyType) - ? _dummyMapping - : null; - - private readonly DummyMapping _dummyMapping = new(); - - private class DummyMapping : RelationalTypeMapping - { - // TODO: The DbType is a hack, we currently require of range subtype mapping that they other expose an NpgsqlDbType - // or a DbType (from which NpgsqlDbType is computed), since RangeTypeMapping sends an NpgsqlDbType. - // This means we currently don't support ranges over types without NpgsqlDbType, which are accessible via - // NpgsqlParameter.DataTypeName - public DummyMapping() - : base("dummy", typeof(DummyType), System.Data.DbType.Guid) - { - } - - private DummyMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } - - protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new DummyMapping(parameters); - } - } - - private class DummyType; - - private class UnknownType; - - protected IModel CreateEmptyModel() - => CreateModelBuilder().Model.FinalizeModel(); - - protected ModelBuilder CreateModelBuilder(Action configureConventions = null) - => NpgsqlTestHelpers.Instance.CreateConventionBuilder(configureConventions: configureConventions); - - #endregion Support -} diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs deleted file mode 100644 index bfefb835a6..0000000000 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ /dev/null @@ -1,1027 +0,0 @@ -using System.Collections; -using System.Collections.Immutable; -using System.Net; -using System.Net.NetworkInformation; -using System.Numerics; -using System.Text.Json; -using Microsoft.EntityFrameworkCore.Design.Internal; -using Microsoft.EntityFrameworkCore.Storage.Json; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage; - -public class NpgsqlTypeMappingTest -{ - #region Numeric - - [Fact] - public void GenerateSqlLiteral_returns_decimal_literal() - { - Assert.Equal( - "1.878787", - GetMapping(typeof(decimal), "numeric").GenerateSqlLiteral(1.878787m)); - - Assert.Equal( - "1.878787", - GetMapping(typeof(float), "numeric").GenerateSqlLiteral(1.878787m)); - - Assert.Equal( - "1.878787", - GetMapping(typeof(double), "numeric").GenerateSqlLiteral(1.878787m)); - } - - #endregion Numeric - - #region Date/Time - - [Fact] - public void DateTime_type_maps_to_timestamptz_by_default() - => Assert.Equal("timestamp with time zone", GetMapping(typeof(DateTime)).StoreType); - - [Fact] - public void Timestamp_maps_to_DateTime_by_default() - => Assert.Same(typeof(DateTime), GetMapping("timestamp without time zone").ClrType); - - [Fact] - public void Timestamptz_maps_to_DateTime_by_default() - => Assert.Same(typeof(DateTime), GetMapping("timestamp with time zone").ClrType); - - [Fact] - public void DateTime_with_precision() - => Assert.Equal( - "timestamp(3) with time zone", - Mapper.FindMapping(typeof(DateTime), "timestamp with time zone", precision: 3)!.StoreType); - - [Fact] - public void GenerateSqlLiteral_returns_date_literal() - { - Assert.Equal( - "DATE '2015-03-12'", - GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(new DateTime(2015, 3, 12))); - - Assert.Equal( - "DATE '2015-03-12'", - GetMapping("date").GenerateSqlLiteral(new DateOnly(2015, 3, 12))); - } - - [Fact] - public void GenerateSqlLiteral_returns_date_infinity_literals() - { - Assert.Equal( - "DATE '-infinity'", - GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(DateTime.MinValue)); - - Assert.Equal( - "DATE 'infinity'", - GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(DateTime.MaxValue)); - - Assert.Equal( - "DATE '-infinity'", - GetMapping("date").GenerateSqlLiteral(DateOnly.MinValue)); - - Assert.Equal( - "DATE 'infinity'", - GetMapping("date").GenerateSqlLiteral(DateOnly.MaxValue)); - } - - [Fact] - public void GenerateSqlLiteral_returns_timestamp_literal() - { - var mapping = GetMapping("timestamp without time zone"); - Assert.Equal( - "TIMESTAMP '1997-12-17T07:37:16'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Local))); - Assert.Equal( - "TIMESTAMP '1997-12-17T07:37:16'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Unspecified))); - Assert.Equal( - "TIMESTAMP '1997-12-17T07:37:16.345'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345))); - } - - [Fact] - public void GenerateSqlLiteral_returns_timestamp_infinity_literals() - { - Assert.Equal( - "TIMESTAMP '-infinity'", - GetMapping("timestamp without time zone").GenerateSqlLiteral(DateTime.MinValue)); - - Assert.Equal( - "TIMESTAMP 'infinity'", - GetMapping("timestamp without time zone").GenerateSqlLiteral(DateTime.MaxValue)); - } - - [Fact] - public void GenerateSqlLiteral_timestamp_does_not_support_utc_datetime() - => Assert.Throws(() => GetMapping("timestamp without time zone").GenerateSqlLiteral(DateTime.UtcNow)); - - [Fact] - public void GenerateSqlLiteral_timestamp_does_not_support_datetimeoffset() - => Assert.Throws( - () => GetMapping("timestamp without time zone").GenerateSqlLiteral(new DateTimeOffset())); - - [Fact] - public void GenerateCodeLiteral_returns_DateTime_utc_literal() - => Assert.Equal( - @"new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Utc)", - CodeLiteral(new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Utc))); - - [Fact] - public void GenerateCodeLiteral_returns_DateTime_local_literal() - => Assert.Equal( - @"new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Local)", - CodeLiteral(new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Local))); - - [Fact] - public void GenerateCodeLiteral_returns_DateTime_unspecified_literal() - => Assert.Equal( - @"new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Unspecified)", - CodeLiteral(new DateTime(2020, 1, 1, 12, 30, 4, 567, DateTimeKind.Unspecified))); - - [Fact] - public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() - { - var mapping = GetMapping("timestamptz"); - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16Z'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Utc))); - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16.345678Z'", - mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345, DateTimeKind.Utc).AddTicks(6780))); - } - - [Fact] - public void GenerateSqlLiteral_returns_timestamptz_infinity_literals() - { - Assert.Equal( - "TIMESTAMPTZ '-infinity'", - GetMapping("timestamptz").GenerateSqlLiteral(DateTime.MinValue)); - - Assert.Equal( - "TIMESTAMPTZ 'infinity'", - GetMapping("timestamptz").GenerateSqlLiteral(DateTime.MaxValue)); - - Assert.Equal( - "TIMESTAMPTZ '-infinity'", - GetMapping(typeof(DateTimeOffset), "timestamptz").GenerateSqlLiteral(DateTimeOffset.MinValue)); - - Assert.Equal( - "TIMESTAMPTZ 'infinity'", - GetMapping(typeof(DateTimeOffset), "timestamptz").GenerateSqlLiteral(DateTimeOffset.MaxValue)); - } - - [Fact] - public void GenerateSqlLiteral_timestamptz_does_not_support_local_datetime() - => Assert.Throws(() => GetMapping("timestamp with time zone").GenerateSqlLiteral(DateTime.Now)); - - [Fact] - public void GenerateSqlLiteral_timestamptz_does_not_support_unspecified_datetime() - => Assert.Throws( - () => GetMapping("timestamp with time zone") - .GenerateSqlLiteral(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified))); - - [Fact] - public void GenerateSqlLiteral_returns_timestamptz_datetimeoffset_literal() - { - var mapping = GetMapping("timestamptz"); - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16+02:00'", - mapping.GenerateSqlLiteral(new DateTimeOffset(1997, 12, 17, 7, 37, 16, TimeSpan.FromHours(2)))); - Assert.Equal( - "TIMESTAMPTZ '1997-12-17T07:37:16.345+02:00'", - mapping.GenerateSqlLiteral(new DateTimeOffset(1997, 12, 17, 7, 37, 16, 345, TimeSpan.FromHours(2)))); - } - - [Fact] - public void GenerateSqlLiteral_returns_time_literal() - { - var mapping = GetMapping("time"); - - Assert.Equal( - "TIME '04:05:06.123456'", - mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6, 123).Add(TimeSpan.FromTicks(4560)))); - Assert.Equal( - "TIME '04:05:06.000123'", - mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6).Add(TimeSpan.FromTicks(1230)))); - Assert.Equal("TIME '04:05:06.123'", mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6, 123))); - Assert.Equal("TIME '04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(4, 5, 6))); - - Assert.Equal( - "TIME '04:05:06.123456'", - mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6, 123).Add(TimeSpan.FromTicks(4560)))); - Assert.Equal( - "TIME '04:05:06.000123'", - mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6).Add(TimeSpan.FromTicks(1230)))); - Assert.Equal("TIME '04:05:06.123'", mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6, 123))); - Assert.Equal("TIME '04:05:06'", mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6))); - Assert.Equal("TIME '13:05:06'", mapping.GenerateSqlLiteral(new TimeOnly(13, 5, 6))); - } - - [Fact] - public void GenerateSqlLiteral_returns_timetz_literal() - { - var mapping = GetMapping("timetz"); - Assert.Equal( - "TIMETZ '04:05:06.123456+3'", - mapping.GenerateSqlLiteral( - new DateTimeOffset(2015, 3, 12, 4, 5, 6, 123, TimeSpan.FromHours(3)) - .AddTicks(4560))); - Assert.Equal( - "TIMETZ '04:05:06.789+3'", mapping.GenerateSqlLiteral(new DateTimeOffset(2015, 3, 12, 4, 5, 6, 789, TimeSpan.FromHours(3)))); - Assert.Equal("TIMETZ '04:05:06-3'", mapping.GenerateSqlLiteral(new DateTimeOffset(2015, 3, 12, 4, 5, 6, TimeSpan.FromHours(-3)))); - } - - [Fact] - public void GenerateSqlLiteral_returns_interval_literal() - { - var mapping = GetMapping("interval"); - Assert.Equal( - "INTERVAL '3 04:05:06.007008'", mapping.GenerateSqlLiteral( - new TimeSpan(3, 4, 5, 6, 7) - .Add(TimeSpan.FromTicks(80)))); - Assert.Equal("INTERVAL '3 04:05:06.007'", mapping.GenerateSqlLiteral(new TimeSpan(3, 4, 5, 6, 7))); - Assert.Equal("INTERVAL '3 04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(3, 4, 5, 6))); - Assert.Equal("INTERVAL '04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(4, 5, 6))); - Assert.Equal("INTERVAL '-3 04:05:06.007'", mapping.GenerateSqlLiteral(new TimeSpan(-3, -4, -5, -6, -7))); - } - - #endregion Date/Time - - #region Networking - - [Fact] - public void GenerateSqlLiteral_returns_macaddr_literal() - => Assert.Equal("MACADDR '001122334455'", GetMapping("macaddr").GenerateSqlLiteral(PhysicalAddress.Parse("00-11-22-33-44-55"))); - - [Fact] - public void GenerateCodeLiteral_returns_macaddr_literal() - => Assert.Equal( - """System.Net.NetworkInformation.PhysicalAddress.Parse("001122334455")""", - CodeLiteral(PhysicalAddress.Parse("00-11-22-33-44-55"))); - - [Fact] - public void GenerateSqlLiteral_returns_macaddr8_literal() - => Assert.Equal( - "MACADDR8 '0011223344556677'", GetMapping("macaddr8").GenerateSqlLiteral(PhysicalAddress.Parse("00-11-22-33-44-55-66-77"))); - - [Fact] - public void GenerateCodeLiteral_returns_macaddr8_literal() - => Assert.Equal( - """System.Net.NetworkInformation.PhysicalAddress.Parse("0011223344556677")""", - CodeLiteral(PhysicalAddress.Parse("00-11-22-33-44-55-66-77"))); - - [Fact] - public void GenerateSqlLiteral_returns_inet_literal() - => Assert.Equal("INET '192.168.1.1'", GetMapping("inet").GenerateSqlLiteral(IPAddress.Parse("192.168.1.1"))); - - [Fact] - public void GenerateCodeLiteral_returns_inet_literal() - => Assert.Equal("""System.Net.IPAddress.Parse("192.168.1.1")""", CodeLiteral(IPAddress.Parse("192.168.1.1"))); - - [Fact] - public void GenerateSqlLiteral_returns_cidr_literal() - => Assert.Equal("CIDR '192.168.1.0/24'", GetMapping("cidr").GenerateSqlLiteral(new IPNetwork(IPAddress.Parse("192.168.1.0"), 24))); - - [Fact] - public void GenerateCodeLiteral_returns_cidr_literal() - => Assert.Equal( - """new System.Net.IPNetwork(System.Net.IPAddress.Parse("192.168.1.0"), 24)""", - CodeLiteral(new IPNetwork(IPAddress.Parse("192.168.1.0"), 24))); - -#pragma warning disable CS0618 // NpgsqlCidr is obsolete, replaced by .NET IPNetwork - [Fact] - public void GenerateSqlLiteral_returns_legacy_cidr_literal() - => Assert.Equal( - "CIDR '192.168.1.0/24'", - GetMapping(typeof(NpgsqlCidr), "cidr").GenerateSqlLiteral(new NpgsqlCidr(IPAddress.Parse("192.168.1.0"), 24))); - - [Fact] - public void GenerateCodeLiteral_returns_legacy_cidr_literal() - => Assert.Equal( - """new NpgsqlTypes.NpgsqlCidr(System.Net.IPAddress.Parse("192.168.1.0"), (byte)24)""", - CodeLiteral(new NpgsqlCidr(IPAddress.Parse("192.168.1.0"), 24))); -#pragma warning restore CS0618 - - #endregion Networking - - #region Geometric - - [Fact] - public void GenerateSqlLiteral_returns_point_literal() - => Assert.Equal("POINT '(3.5,4.5)'", GetMapping("point").GenerateSqlLiteral(new NpgsqlPoint(3.5, 4.5))); - - [Fact] - public void GenerateCodeLiteral_returns_point_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlPoint(3.5, 4.5)", CodeLiteral(new NpgsqlPoint(3.5, 4.5))); - - [Fact] - public void GenerateSqlLiteral_returns_line_literal() - => Assert.Equal("LINE '{3.5,4.5,10}'", GetMapping("line").GenerateSqlLiteral(new NpgsqlLine(3.5, 4.5, 10))); - - [Fact] - public void GenerateCodeLiteral_returns_line_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlLine(3.5, 4.5, 10.0)", CodeLiteral(new NpgsqlLine(3.5, 4.5, 10))); - - [Fact] - public void GenerateSqlLiteral_returns_lseg_literal() - => Assert.Equal("LSEG '[(3.5,4.5),(5.5,6.5)]'", GetMapping("lseg").GenerateSqlLiteral(new NpgsqlLSeg(3.5, 4.5, 5.5, 6.5))); - - [Fact] - public void GenerateCodeLiteral_returns_lseg_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlLSeg(3.5, 4.5, 5.5, 6.5)", CodeLiteral(new NpgsqlLSeg(3.5, 4.5, 5.5, 6.5))); - - [Fact] - public void GenerateSqlLiteral_returns_box_literal() - => Assert.Equal("BOX '((4,3),(2,1))'", GetMapping("box").GenerateSqlLiteral(new NpgsqlBox(1, 2, 3, 4))); - - [Fact] - public void GenerateCodeLiteral_returns_box_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlBox(3.0, 4.0, 1.0, 2.0)", CodeLiteral(new NpgsqlBox(1, 2, 3, 4))); - - [Fact] - public void GenerateSqlLiteral_returns_path_closed_literal() - => Assert.Equal( - "PATH '((1,2),(3,4))'", GetMapping("path").GenerateSqlLiteral( - new NpgsqlPath( - new NpgsqlPoint(1, 2), - new NpgsqlPoint(3, 4) - ))); - - [Fact] - public void GenerateCodeLiteral_returns_closed_path_literal() - => Assert.Equal( - "new NpgsqlTypes.NpgsqlPath(new NpgsqlPoint[] { new NpgsqlTypes.NpgsqlPoint(1.0, 2.0), new NpgsqlTypes.NpgsqlPoint(3.0, 4.0) }, false)", - CodeLiteral( - new NpgsqlPath( - new NpgsqlPoint(1, 2), - new NpgsqlPoint(3, 4) - ))); - - [Fact] - public void GenerateSqlLiteral_returns_path_open_literal() - => Assert.Equal( - "PATH '[(1,2),(3,4)]'", GetMapping("path").GenerateSqlLiteral( - new NpgsqlPath( - new NpgsqlPoint(1, 2), - new NpgsqlPoint(3, 4) - ) { Open = true })); - - [Fact] - public void GenerateCodeLiteral_returns_open_path_literal() - => Assert.Equal( - "new NpgsqlTypes.NpgsqlPath(new NpgsqlPoint[] { new NpgsqlTypes.NpgsqlPoint(1.0, 2.0), new NpgsqlTypes.NpgsqlPoint(3.0, 4.0) }, true)", - CodeLiteral( - new NpgsqlPath( - new NpgsqlPoint(1, 2), - new NpgsqlPoint(3, 4) - ) { Open = true })); - - [Fact] - public void GenerateSqlLiteral_returns_polygon_literal() - => Assert.Equal( - "POLYGON '((1,2),(3,4))'", GetMapping("polygon").GenerateSqlLiteral( - new NpgsqlPolygon( - new NpgsqlPoint(1, 2), - new NpgsqlPoint(3, 4) - ))); - - [Fact] - public void GenerateCodeLiteral_returns_polygon_literal() - => Assert.Equal( - "new NpgsqlTypes.NpgsqlPolygon(new NpgsqlPoint[] { new NpgsqlTypes.NpgsqlPoint(1.0, 2.0), new NpgsqlTypes.NpgsqlPoint(3.0, 4.0) })", - CodeLiteral( - new NpgsqlPolygon( - new NpgsqlPoint(1, 2), - new NpgsqlPoint(3, 4) - ))); - - [Fact] - public void GenerateSqlLiteral_returns_circle_literal() - => Assert.Equal("CIRCLE '<(3.5,4.5),5.5>'", GetMapping("circle").GenerateSqlLiteral(new NpgsqlCircle(3.5, 4.5, 5.5))); - - [Fact] - public void GenerateCodeLiteral_returns_circle_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlCircle(3.5, 4.5, 5.5)", CodeLiteral(new NpgsqlCircle(3.5, 4.5, 5.5))); - - #endregion Geometric - - #region Misc - - [Fact] - public void GenerateSqlLiteral_returns_bool_literal() - => Assert.Equal("TRUE", GetMapping("bool").GenerateSqlLiteral(true)); - - [Fact] - public void GenerateSqlLiteral_returns_varbit_literal() - => 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([true, false]))); - - [Fact] - public void GenerateSqlLiteral_returns_bit_literal() - => 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([true, false]))); - - [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] - public void ValueComparer_hstore_array() - { - // This exercises array's comparer when the element has its own non-null comparer - var source = new[] { new Dictionary { { "k1", "v1" } }, new Dictionary { { "k2", "v2" } }, }; - - var comparer = GetMapping(typeof(Dictionary[])).Comparer; - var snapshot = (Dictionary[])comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - source[1]["k2"] = "v8"; - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact] - public void GenerateSqlLiteral_returns_bytea_literal() - => Assert.Equal(@"BYTEA E'\\xDEADBEEF'", GetMapping("bytea").GenerateSqlLiteral(new byte[] { 222, 173, 190, 239 })); - - [Fact] - public void GenerateSqlLiteral_returns_hstore_literal() - => Assert.Equal( - """HSTORE '"k1"=>"v1","k2"=>"v2"'""", - GetMapping("hstore").GenerateSqlLiteral(new Dictionary { { "k1", "v1" }, { "k2", "v2" } })); - - [Fact] - public void GenerateSqlLiteral_returns_BigInteger_literal() - { - var mapping = GetMapping(typeof(BigInteger)); - - Assert.Equal(@"0", mapping.GenerateSqlLiteral(BigInteger.Zero)); - Assert.Equal(int.MaxValue.ToString(), mapping.GenerateSqlLiteral(new BigInteger(int.MaxValue))); - Assert.Equal(int.MinValue.ToString(), mapping.GenerateSqlLiteral(new BigInteger(int.MinValue))); - } - - [Fact] - public void GenerateCodeLiteral_returns_BigInteger_literal() - => Assert.Equal( - """BigInteger.Parse("18446744073709551615", NumberFormatInfo.InvariantInfo)""", - CodeLiteral(new BigInteger(ulong.MaxValue))); - - [Fact] - public void ValueComparer_hstore_as_Dictionary() - { - var source = new Dictionary { { "k1", "v1" }, { "k2", "v2" } }; - - var comparer = GetMapping("hstore").Comparer; - var snapshot = (Dictionary)comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - snapshot.Remove("k1"); - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact] - public void ValueComparer_hstore_as_ImmutableDictionary() - { - var source = ImmutableDictionary.Empty - .Add("k1", "v1") - .Add("k2", "v2"); - - var comparer = Mapper.FindMapping(typeof(ImmutableDictionary), "hstore").Comparer; - var snapshot = comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - source = source.Remove("k1"); - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact] - public void GenerateSqlLiteral_returns_enum_literal() - { - 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)); - } - - [Fact] - public void GenerateSqlLiteral_returns_enum_uppercase_literal() - { - var mapping = new NpgsqlEnumTypeMapping(""" - "DummyEnum" - """, "DummyEnum", typeof(DummyEnum), new Dictionary - { - [DummyEnum.Happy] = "happy", - [DummyEnum.Sad] = "sad" - }); - - Assert.Equal(""" - 'sad'::"DummyEnum" - """, mapping.GenerateSqlLiteral(DummyEnum.Sad)); - } - - private enum DummyEnum - { - // ReSharper disable once UnusedMember.Local - Happy, - Sad - } - - [Fact] - public void GenerateSqlLiteral_returns_tid_literal() - => Assert.Equal(@"TID '(0,1)'", GetMapping("tid").GenerateSqlLiteral(new NpgsqlTid(0, 1))); - - [Fact] - public void GenerateSqlLiteral_returns_pg_lsn_literal() - => Assert.Equal(@"PG_LSN '12345/67890'", GetMapping("pg_lsn").GenerateSqlLiteral(NpgsqlLogSequenceNumber.Parse("12345/67890"))); - - #endregion Misc - - #region Array - - [Fact] - public void GenerateSqlLiteral_returns_array_literal() - => Assert.Equal("ARRAY[3,4]::integer[]", GetMapping(typeof(int[])).GenerateSqlLiteral(new[] { 3, 4 })); - - [Fact] - public void GenerateSqlLiteral_returns_array_empty_literal() - => Assert.Equal("ARRAY[]::smallint[]", GetMapping(typeof(short[])).GenerateSqlLiteral(Array.Empty())); - - [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] - public void ValueComparer_int_array() - { - // This exercises array's comparer when the element doesn't have a comparer, but it implements - // IEquatable - var source = new[] { 2, 3, 4 }; - - var comparer = GetMapping(typeof(int[])).Comparer; - var snapshot = (int[])comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - snapshot[1] = 8; - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] - public void ValueComparer_int_list() - { - var source = new List - { - 2, - 3, - 4 - }; - - var comparer = GetMapping(typeof(List)).Comparer; - var snapshot = (List)comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - snapshot[1] = 8; - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] - public void ValueComparer_nullable_int_array() - { - var source = new int?[] { 2, 3, 4, null }; - - var comparer = GetMapping(typeof(int?[])).Comparer; - var snapshot = (int?[])comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - snapshot[1] = 8; - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] - public void ValueComparer_nullable_int_list() - { - var source = new List - { - 2, - 3, - 4, - null - }; - - var comparer = GetMapping(typeof(List)).Comparer; - var snapshot = (List)comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - snapshot[1] = 8; - Assert.False(comparer.Equals(source, snapshot)); - } - - [Fact(Skip = "https://github.com/dotnet/efcore/pull/30939")] - public void ValueComparer_nullable_array_with_iequatable_element() - { - var source = new NpgsqlPoint?[] { new NpgsqlPoint(1, 1), null }; - - var comparer = GetMapping(typeof(NpgsqlPoint?[])).Comparer; - var snapshot = (NpgsqlPoint?[])comparer.Snapshot(source); - Assert.Equal(source, snapshot); - Assert.NotSame(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - snapshot[1] = new NpgsqlPoint(2, 2); - Assert.False(comparer.Equals(source, snapshot)); - } - - #endregion Array - - #region Ranges - - [Fact] - public void GenerateSqlLiteral_returns_range_empty_literal() - { - var value = NpgsqlRange.Empty; - var literal = GetMapping("int4range").GenerateSqlLiteral(value); - Assert.Equal("'empty'::int4range", literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_range_empty_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlRange(0, false, 0, false)", CodeLiteral(NpgsqlRange.Empty)); - - [Fact] - public void GenerateSqlLiteral_returns_range_inclusive_literal() - { - var value = new NpgsqlRange(4, 7); - var literal = GetMapping("int4range").GenerateSqlLiteral(value); - Assert.Equal("'[4,7]'::int4range", literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_range_inclusive_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlRange(4, 7)", CodeLiteral(new NpgsqlRange(4, 7))); - - [Fact] - public void GenerateSqlLiteral_returns_range_inclusive_exclusive_literal() - { - var value = new NpgsqlRange(4, false, 7, true); - var literal = GetMapping("int4range").GenerateSqlLiteral(value); - Assert.Equal("'(4,7]'::int4range", literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_range_inclusive_exclusive_literal() - => Assert.Equal("new NpgsqlTypes.NpgsqlRange(4, false, 7, true)", CodeLiteral(new NpgsqlRange(4, false, 7, true))); - - [Fact] - public void GenerateSqlLiteral_returns_range_infinite_literal() - { - var value = new NpgsqlRange(0, false, true, 7, true, false); - var literal = GetMapping("int4range").GenerateSqlLiteral(value); - Assert.Equal("'(,7]'::int4range", literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_range_infinite_literal() - => Assert.Equal( - "new NpgsqlTypes.NpgsqlRange(0, false, true, 7, true, false)", - CodeLiteral(new NpgsqlRange(0, false, true, 7, true, false))); - - // Tests for the built-in ranges - - [Fact] - public void GenerateSqlLiteral_returns_int4range_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping("int4range"); - Assert.Equal("integer", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange(4, 7); - Assert.Equal("'[4,7]'::int4range", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_int8range_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping("int8range"); - Assert.Equal("bigint", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange(4, 7); - Assert.Equal("'[4,7]'::int8range", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_numrange_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping("numrange"); - Assert.Equal("numeric", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange(4, 7); - Assert.Equal("'[4.0,7.0]'::numrange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tsrange_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping("tsrange"); - 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)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping("tstzrange"); - Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); - - 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)); - } - - [Fact] - public void GenerateSqlLiteral_returns_daterange_DateOnly_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping("daterange"); - Assert.Equal("date", mapping.SubtypeMapping.StoreType); - Assert.Equal("date", ((NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange))).SubtypeMapping.StoreType); - - var value = new NpgsqlRange(new DateOnly(2020, 1, 1), new DateOnly(2020, 1, 2)); - Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_daterange_DateTime_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange), "daterange"); - Assert.Equal("date", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange(new DateTime(2020, 1, 1), new DateTime(2020, 1, 2)); - Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); - } - - #endregion Ranges - - #region Multiranges - - [Fact] - public void GenerateSqlLiteral_returns_multirange_literal() - { - var value = new NpgsqlRange[] - { - new(4, 7), - new(9, lowerBoundIsInclusive: true, 10, upperBoundIsInclusive: false), - new( - 13, lowerBoundIsInclusive: false, lowerBoundInfinite: false, default, upperBoundIsInclusive: false, - upperBoundInfinite: true) - }; - var literal = GetMapping("int4multirange").GenerateSqlLiteral(value); - Assert.Equal("'{[4,7], [9,10), (13,)}'::int4multirange", literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_multirange_array_literal() - { - var value = new NpgsqlRange[] - { - new(4, 7), - new(9, lowerBoundIsInclusive: true, 10, upperBoundIsInclusive: false), - new( - 13, lowerBoundIsInclusive: false, lowerBoundInfinite: false, default, upperBoundIsInclusive: false, - upperBoundInfinite: true) - }; - var literal = CodeLiteral(value); - Assert.Equal( - "new[] { new NpgsqlTypes.NpgsqlRange(4, 7), new NpgsqlTypes.NpgsqlRange(9, true, 10, false), new NpgsqlTypes.NpgsqlRange(13, false, false, 0, false, true) }", - literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_multirange_list_literal() - { - var value = new List> - { - new(4, 7), - new(9, lowerBoundIsInclusive: true, 10, upperBoundIsInclusive: false), - new( - 13, lowerBoundIsInclusive: false, lowerBoundInfinite: false, default, upperBoundIsInclusive: false, - upperBoundInfinite: true) - }; - var literal = CodeLiteral(value); - Assert.Equal( - "new List> { new NpgsqlTypes.NpgsqlRange(4, 7), new NpgsqlTypes.NpgsqlRange(9, true, 10, false), new NpgsqlTypes.NpgsqlRange(13, false, false, 0, false, true) }", - literal); - } - - [Fact] - public void GenerateSqlLiteral_returns_multirange_empty_literal() - { - var value = Array.Empty>(); - var literal = GetMapping("int4multirange").GenerateSqlLiteral(value); - Assert.Equal("'{}'::int4multirange", literal); - } - - [Fact] - public void GenerateCodeLiteral_returns_multirange_empty_array_literal() - { - var value = Array.Empty>(); - var literal = CodeLiteral(value); - Assert.Equal("new NpgsqlRange[0]", literal); - } - - #endregion Multiranges - - #region Full text search - -#pragma warning disable CS0618 // Full-text search client-parsing is obsolete - [Fact] - public void GenerateSqlLiteral_returns_tsquery_literal() - => Assert.Equal( - @"TSQUERY '''a'' & ''b'''", - GetMapping("tsquery").GenerateSqlLiteral(NpgsqlTsQuery.Parse("a & b"))); - - [Fact] - public void GenerateSqlLiteral_returns_tsvector_literal() - => Assert.Equal( - @"TSVECTOR '''a'' ''b'''", - GetMapping("tsvector").GenerateSqlLiteral(NpgsqlTsVector.Parse("a b"))); -#pragma warning restore CS0618 - - [Fact] - public void GenerateSqlLiteral_returns_ranking_normalization_literal() - => Assert.Equal( - $"{(int)NpgsqlTsRankingNormalization.DivideByLength}", - GetMapping(typeof(NpgsqlTsRankingNormalization)) - .GenerateSqlLiteral(NpgsqlTsRankingNormalization.DivideByLength)); - - #endregion Full text search - - #region Json - - [Fact] - public void GenerateSqlLiteral_returns_jsonb_string_literal() - => Assert.Equal("""'{"a":1}'""", GetMapping("jsonb").GenerateSqlLiteral("""{"a":1}""")); - - [Fact] - public void GenerateSqlLiteral_returns_json_string_literal() - => Assert.Equal("""'{"a":1}'""", GetMapping("json").GenerateSqlLiteral("""{"a":1}""")); - - [Fact] - public void GenerateSqlLiteral_returns_jsonb_object_literal() - { - var literal = Mapper.FindMapping(typeof(Customer), "jsonb").GenerateSqlLiteral(SampleCustomer); - Assert.Equal( - """'{"Name":"Joe","Age":25,"IsVip":false,"Orders":[{"Price":99.5,"ShippingAddress":"Some address 1","ShippingDate":"2019-10-01T00:00:00"},{"Price":23,"ShippingAddress":"Some address 2","ShippingDate":"2019-10-10T00:00:00"}]}'""", - literal); - } - - [Fact] - public void GenerateSqlLiteral_returns_json_object_literal() - { - var literal = Mapper.FindMapping(typeof(Customer), "json").GenerateSqlLiteral(SampleCustomer); - Assert.Equal( - """'{"Name":"Joe","Age":25,"IsVip":false,"Orders":[{"Price":99.5,"ShippingAddress":"Some address 1","ShippingDate":"2019-10-01T00:00:00"},{"Price":23,"ShippingAddress":"Some address 2","ShippingDate":"2019-10-10T00:00:00"}]}'""", - literal); - } - - [Fact] - public void GenerateSqlLiteral_returns_jsonb_document_literal() - { - var json = """{"Name":"Joe","Age":25}"""; - var literal = Mapper.FindMapping(typeof(JsonDocument), "jsonb").GenerateSqlLiteral(JsonDocument.Parse(json)); - Assert.Equal($"'{json}'", literal); - } - - [Fact] - public void GenerateSqlLiteral_returns_json_document_literal() - { - var json = """{"Name":"Joe","Age":25}"""; - var literal = Mapper.FindMapping(typeof(JsonDocument), "json").GenerateSqlLiteral(JsonDocument.Parse(json)); - Assert.Equal($"'{json}'", literal); - } - - [Fact] - public void GenerateSqlLiteral_returns_jsonb_element_literal() - { - var json = """{"Name":"Joe","Age":25}"""; - var literal = Mapper.FindMapping(typeof(JsonElement), "jsonb").GenerateSqlLiteral(JsonDocument.Parse(json).RootElement); - Assert.Equal($"'{json}'", literal); - } - - [Fact] - public void GenerateSqlLiteral_returns_json_element_literal() - { - var json = """{"Name":"Joe","Age":25}"""; - var literal = Mapper.FindMapping(typeof(JsonElement), "json").GenerateSqlLiteral(JsonDocument.Parse(json).RootElement); - Assert.Equal($"'{json}'", literal); - } - - [Fact] - 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}"""))); - - [Fact] - public void GenerateCodeLiteral_returns_json_element_literal() - => Assert.Equal( - """System.Text.Json.JsonDocument.Parse("{\"Name\":\"Joe\",\"Age\":25}", new System.Text.Json.JsonDocumentOptions()).RootElement""", - CodeLiteral(JsonDocument.Parse(@"{""Name"":""Joe"",""Age"":25}").RootElement)); - - [Fact] - public void ValueComparer_JsonDocument() - { - var json = """{"Name":"Joe","Age":25}"""; - var source = JsonDocument.Parse(json); - - var comparer = GetMapping(typeof(JsonDocument)).Comparer; - var snapshot = (JsonDocument)comparer.Snapshot(source); - Assert.Same(source, snapshot); - Assert.True(comparer.Equals(source, snapshot)); - } - - [Fact] - public void ValueComparer_JsonElement() - { - var json = """{"Name":"Joe","Age":25}"""; - var source = JsonDocument.Parse(json).RootElement; - - var comparer = GetMapping(typeof(JsonElement)).Comparer; - var snapshot = (JsonElement)comparer.Snapshot(source); - Assert.True(comparer.Equals(source, snapshot)); - Assert.False(comparer.Equals(source, JsonDocument.Parse(json).RootElement)); - } - - private static readonly Customer SampleCustomer = new() - { - Name = "Joe", - Age = 25, - IsVip = false, - Orders = - [ - new Order - { - Price = 99.5m, - ShippingAddress = "Some address 1", - ShippingDate = new DateTime(2019, 10, 1) - }, - new Order - { - Price = 23, - ShippingAddress = "Some address 2", - ShippingDate = new DateTime(2019, 10, 10) - } - ] - }; - - public class Customer - { - public string Name { get; set; } - public int Age { get; set; } - public bool IsVip { get; set; } - public Order[] Orders { get; set; } - } - - public class Order - { - public decimal Price { get; set; } - public string ShippingAddress { get; set; } - public DateTime ShippingDate { get; set; } - } - - #endregion Json - - #region Support - - private static readonly NpgsqlTypeMappingSource Mapper = new( - new TypeMappingSourceDependencies( - new ValueConverterSelector(new ValueConverterSelectorDependencies()), - new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), - [] - ), - new RelationalTypeMappingSourceDependencies([]), - 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 static readonly CSharpHelper CsHelper = new(Mapper); - - private static string CodeLiteral(object value) - => CsHelper.UnknownLiteral(value); - - #endregion Support -} diff --git a/test/EFCore.PG.Tests/Types/LTreeTest.cs b/test/EFCore.PG.Tests/Types/LTreeTest.cs deleted file mode 100644 index a3118467ff..0000000000 --- a/test/EFCore.PG.Tests/Types/LTreeTest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Types; - -public class LTreeTest -{ - [ConditionalFact] - public void ToString_works() - => Assert.Equal("Top.Sub", ((LTree)"Top.Sub").ToString()); -} diff --git a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs b/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs deleted file mode 100644 index 0f4ad43b3e..0000000000 --- a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchFactoryTest.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure.Internal; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using Microsoft.EntityFrameworkCore.Update.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -using Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Update; - -public class NpgsqlModificationCommandBatchFactoryTest -{ - [Fact] - public void Uses_MaxBatchSize_specified_in_NpgsqlOptionsExtension() - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Database=Crunchie", b => b.MaxBatchSize(1)); - - var typeMapper = new NpgsqlTypeMappingSource( - TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create(), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSingletonOptions()); - - var factory = new NpgsqlModificationCommandBatchFactory( - new ModificationCommandBatchFactoryDependencies( - new RelationalCommandBuilderFactory( - new RelationalCommandBuilderDependencies( - typeMapper, - new ExceptionDetector(), - new LoggingOptions())), - new NpgsqlSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()), - new NpgsqlUpdateSqlGenerator( - new UpdateSqlGeneratorDependencies( - new NpgsqlSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()), - typeMapper)), - new CurrentDbContext(new FakeDbContext()), - new FakeRelationalCommandDiagnosticsLogger(), - new FakeDiagnosticsLogger()), - optionsBuilder.Options); - - var batch = factory.Create(); - - Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); - Assert.False(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); - } - - [Fact] - public void MaxBatchSize_is_optional() - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql("Database=Crunchie"); - - var typeMapper = new NpgsqlTypeMappingSource( - TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create(), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSingletonOptions()); - - var factory = new NpgsqlModificationCommandBatchFactory( - new ModificationCommandBatchFactoryDependencies( - new RelationalCommandBuilderFactory( - new RelationalCommandBuilderDependencies( - typeMapper, - new ExceptionDetector(), - new LoggingOptions())), - new NpgsqlSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()), - new NpgsqlUpdateSqlGenerator( - new UpdateSqlGeneratorDependencies( - new NpgsqlSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()), - typeMapper)), - new CurrentDbContext(new FakeDbContext()), - new FakeRelationalCommandDiagnosticsLogger(), - new FakeDiagnosticsLogger()), - optionsBuilder.Options); - - var batch = factory.Create(); - - Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); - Assert.True(batch.TryAddCommand(CreateModificationCommand("T1", null, false))); - } - - private class FakeDbContext : DbContext; - - private static INonTrackedModificationCommand CreateModificationCommand( - string name, - string schema, - bool sensitiveLoggingEnabled) - => new ModificationCommandFactory().CreateNonTrackedModificationCommand( - new NonTrackedModificationCommandParameters(name, schema, sensitiveLoggingEnabled)); -} diff --git a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs b/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs deleted file mode 100644 index d9a254a620..0000000000 --- a/test/EFCore.PG.Tests/Update/NpgsqlModificationCommandBatchTest.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure.Internal; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using Microsoft.EntityFrameworkCore.Update.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -using Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal; - -// ReSharper disable once CheckNamespace -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Tests.Update; - -public class NpgsqlModificationCommandBatchTest -{ - [Fact] - public void AddCommand_returns_false_when_max_batch_size_is_reached() - { - var typeMapper = new NpgsqlTypeMappingSource( - TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create(), - new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()), - new NpgsqlSingletonOptions()); - - var batch = new NpgsqlModificationCommandBatch( - new ModificationCommandBatchFactoryDependencies( - new RelationalCommandBuilderFactory( - new RelationalCommandBuilderDependencies( - typeMapper, - new ExceptionDetector(), - new LoggingOptions())), - new NpgsqlSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()), - new NpgsqlUpdateSqlGenerator( - new UpdateSqlGeneratorDependencies( - new NpgsqlSqlGenerationHelper( - new RelationalSqlGenerationHelperDependencies()), - typeMapper)), - new CurrentDbContext(new FakeDbContext()), - new FakeRelationalCommandDiagnosticsLogger(), - new FakeDiagnosticsLogger()), - maxBatchSize: 1); - - Assert.True( - batch.TryAddCommand( - CreateModificationCommand("T1", null, false))); - Assert.False( - batch.TryAddCommand( - CreateModificationCommand("T1", null, false))); - } - - private class FakeDbContext : DbContext; - - private static INonTrackedModificationCommand CreateModificationCommand( - string name, - string schema, - bool sensitiveLoggingEnabled) - => new ModificationCommandFactory().CreateNonTrackedModificationCommand( - new NonTrackedModificationCommandParameters(name, schema, sensitiveLoggingEnabled)); -}