From 1f1aad9e50a3705a4254439189d0dbd9b5a5a91e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ausw=C3=B6ger?= Date: Sun, 5 Nov 2023 20:21:38 +0100 Subject: [PATCH 1/7] Fix MariaDB list columns performance (#6202) | Q | A |------------- | ----------- | Type | bug | Fixed issues | https://github.com/contao/contao/issues/6409 #### Summary Starting with version 3.7.0 we noticed that the `listTableColumns()` method got very slow on some systems, our migrate command went from taking around 2 seconds with version 3.6 to 260 seconds with version3.7. This is due to the `LEFT JOIN information_schema.CHECK_CONSTRAINTS` not getting optimized by MariaDB which causes the database server to scan every table of every database. This pull request fixes that by using a subquery that is limited by `WHERE CONSTRAINT_SCHEMA = $databaseName`. This brings the performance back to where it was with version 3.6. --- src/Platforms/AbstractMySQLPlatform.php | 12 +++++- src/Platforms/MariaDb1043Platform.php | 39 ++++++++++--------- src/Schema/MySQLSchemaManager.php | 3 +- .../Schema/MySQLSchemaManagerTest.php | 6 +++ 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index d1e3e5aff52..284194d9dc7 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -406,7 +406,17 @@ public function getListTableColumnsSQL($table, $database = null) */ public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array { - return [$tableAlias . '.COLUMN_TYPE', '']; + return [$this->getColumnTypeSQLSnippet(...func_get_args()), '']; + } + + /** + * The SQL snippet required to elucidate a column type + * + * Returns a column type SELECT snippet string + */ + public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $databaseName = null): string + { + return $tableAlias . '.COLUMN_TYPE'; } /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ diff --git a/src/Platforms/MariaDb1043Platform.php b/src/Platforms/MariaDb1043Platform.php index 926c6b9529d..8ba6792ed01 100644 --- a/src/Platforms/MariaDb1043Platform.php +++ b/src/Platforms/MariaDb1043Platform.php @@ -40,7 +40,8 @@ public function getJsonTypeDeclarationSQL(array $column): string */ public function getListTableColumnsSQL($table, $database = null): string { - [$columnTypeSQL, $joinCheckConstraintSQL] = $this->getColumnTypeSQLSnippets(); + // @todo 4.0 - call getColumnTypeSQLSnippet() instead + [$columnTypeSQL, $joinCheckConstraintSQL] = $this->getColumnTypeSQLSnippets('c', $database); return sprintf( <<getJsonTypeDeclarationSQL([]) !== 'JSON') { - return parent::getColumnTypeSQLSnippets($tableAlias); + return parent::getColumnTypeSQLSnippet($tableAlias, $databaseName); } - $columnTypeSQL = <<getDatabaseNameSQL($databaseName); + + // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues + return <<_platform->getColumnTypeSQLSnippets(); + // @todo 4.0 - call getColumnTypeSQLSnippet() instead + [$columnTypeSQL, $joinCheckConstraintSQL] = $this->_platform->getColumnTypeSQLSnippets('c', $databaseName); $sql = 'SELECT'; diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index 3e90d6aabed..519360928bd 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDb1027Platform; +use Doctrine\DBAL\Platforms\MariaDb1043Platform; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Tests\Functional\Schema\MySQL\PointType; @@ -401,6 +402,11 @@ public function testJsonColumnType(): void $table->addColumn('col_json', Types::JSON); $this->dropAndCreateTable($table); + // Remove the comment from the column to ensure the type is detected correctly from the check constraints. + if ($this->connection->getDatabasePlatform() instanceof MariaDb1043Platform) { + $this->connection->executeStatement('ALTER TABLE test_mysql_json CHANGE COLUMN col_json col_json JSON'); + } + $columns = $this->schemaManager->listTableColumns('test_mysql_json'); self::assertSame(Types::JSON, $columns['col_json']->getType()->getName()); From 39fe3b44dab35465dfa7c47ced7e6291698104d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ausw=C3=B6ger?= Date: Sun, 5 Nov 2023 22:42:24 +0100 Subject: [PATCH 2/7] Deprecate AbstractMySQLPlatform::getColumnTypeSQLSnippets() (#6213) | Q | A |------------- | ----------- | Type | improvement | Fixed issues | https://github.com/doctrine/dbal/pull/6202#discussion_r1382622523 #### Summary Deprecates `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` ~~Needs to be rebased after https://github.com/doctrine/dbal/pull/6202 got merged upstream.~~ Done. --------- Co-authored-by: Alexander M. Turek --- UPGRADE.md | 5 +++++ psalm.xml.dist | 5 +++++ src/Platforms/AbstractMySQLPlatform.php | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index c27231a450c..43d648159be 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,6 +8,11 @@ awareness about deprecated code. # Upgrade to 3.8 +## Deprecated `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` + +`AbstractMySQLPlatform::getColumnTypeSQLSnippets()` has been deprecated. +Use `AbstractMySQLPlatform::getColumnTypeSQLSnippet()` instead. + ## Deprecated reset methods from `QueryBuilder` `QueryBuilder::resetQueryParts()` has been deprecated. diff --git a/psalm.xml.dist b/psalm.xml.dist index bfeea5a7222..eb12a6885fc 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -506,6 +506,11 @@ + + diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index 284194d9dc7..485d4d8bb8e 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -398,6 +398,8 @@ public function getListTableColumnsSQL($table, $database = null) } /** + * @deprecated Use {@see getColumnTypeSQLSnippet()} instead. + * * The SQL snippets required to elucidate a column type * * Returns an array of the form [column type SELECT snippet, additional JOIN statement snippet] @@ -406,6 +408,13 @@ public function getListTableColumnsSQL($table, $database = null) */ public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6202', + 'AbstractMySQLPlatform::getColumnTypeSQLSnippets() is deprecated. ' + . 'Use AbstractMySQLPlatform::getColumnTypeSQLSnippet() instead.', + ); + return [$this->getColumnTypeSQLSnippet(...func_get_args()), '']; } From 4dd07963c113921629dcae87bc54c180a3f8b84a Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 6 Nov 2023 19:50:08 +0100 Subject: [PATCH 3/7] Extend `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` deprecation layer (#6215) | Q | A |------------- | ----------- | Type | improvement | Fixed issues | Follows #6213 #### Summary We want to stop guessing the current database. cc @ausi --- UPGRADE.md | 8 +++++--- psalm.xml.dist | 1 + src/Platforms/AbstractMySQLPlatform.php | 8 ++++++++ src/Platforms/MariaDb1043Platform.php | 11 +++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 43d648159be..06e70fdcd3c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,10 +8,12 @@ awareness about deprecated code. # Upgrade to 3.8 -## Deprecated `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` +## Deprecated `AbstractMySQLPlatform` methods -`AbstractMySQLPlatform::getColumnTypeSQLSnippets()` has been deprecated. -Use `AbstractMySQLPlatform::getColumnTypeSQLSnippet()` instead. +* `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` has been deprecated + in favor of `AbstractMySQLPlatform::getColumnTypeSQLSnippet()`. +* `AbstractMySQLPlatform::getDatabaseNameSQL()` has been deprecated without replacement. +* Not passing a database name to `AbstractMySQLPlatform::getColumnTypeSQLSnippet()` has been deprecated. ## Deprecated reset methods from `QueryBuilder` diff --git a/psalm.xml.dist b/psalm.xml.dist index eb12a6885fc..0eb1e5ced86 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -511,6 +511,7 @@ TODO: remove in 4.0.0 --> + diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index 485d4d8bb8e..bc409bdf4cb 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -1422,8 +1422,16 @@ public function supportsColumnLengthIndexes(): bool return true; } + /** @deprecated Will be removed without replacement. */ protected function getDatabaseNameSQL(?string $databaseName): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6215', + '%s is deprecated without replacement.', + __METHOD__, + ); + if ($databaseName !== null) { return $this->quoteStringLiteral($databaseName); } diff --git a/src/Platforms/MariaDb1043Platform.php b/src/Platforms/MariaDb1043Platform.php index 8ba6792ed01..5f51ac34cc9 100644 --- a/src/Platforms/MariaDb1043Platform.php +++ b/src/Platforms/MariaDb1043Platform.php @@ -3,6 +3,7 @@ namespace Doctrine\DBAL\Platforms; use Doctrine\DBAL\Types\JsonType; +use Doctrine\Deprecations\Deprecation; use function sprintf; @@ -81,6 +82,16 @@ public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $datab return parent::getColumnTypeSQLSnippet($tableAlias, $databaseName); } + if ($databaseName === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6215', + 'Not passing a database name to methods "getColumnTypeSQLSnippet()", ' + . '"getColumnTypeSQLSnippets()", and "getListTableColumnsSQL()" of "%s" is deprecated.', + self::class, + ); + } + $databaseName = $this->getDatabaseNameSQL($databaseName); // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues From 2caad211fc51713d7e803545d603270d9fbb8d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 6 Nov 2023 19:52:47 +0100 Subject: [PATCH 4/7] Add Oracle v23 to CI testing (#6217) | Q | A |------------- | ----------- | Type | improvement | Fixed issues | n/a #### Summary Latest Oracle database compatibility should be tested. Targetting lowest open branch to assert the compatibility for 3.7.x too for possible bugfixes. --- .github/workflows/continuous-integration.yml | 10 +++-- ci/github/phpunit/oci8-21.xml | 39 ++++++++++++++++++++ ci/github/phpunit/oci8.xml | 4 +- ci/github/phpunit/pdo_oci-21.xml | 39 ++++++++++++++++++++ ci/github/phpunit/pdo_oci.xml | 4 +- 5 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 ci/github/phpunit/oci8-21.xml create mode 100644 ci/github/phpunit/pdo_oci-21.xml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e31d90e4bae..4eca2d350fc 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -111,13 +111,14 @@ jobs: - "8.3" oracle-version: - "21" + - "23" include: - php-version: "7.4" oracle-version: "11" services: oracle: - image: gvenzl/oracle-xe:${{ matrix.oracle-version }} + image: gvenzl/oracle-${{ matrix.oracle-version < 23 && 'xe' || 'free' }}:${{ matrix.oracle-version }} env: ORACLE_PASSWORD: oracle ports: @@ -148,7 +149,7 @@ jobs: composer-options: "--ignore-platform-req=php+" - name: "Run PHPUnit" - run: "vendor/bin/phpunit -c ci/github/phpunit/oci8.xml --coverage-clover=coverage.xml" + run: "vendor/bin/phpunit -c ci/github/phpunit/oci8${{ matrix.oracle-version < 23 && '-21' || '' }}.xml --coverage-clover=coverage.xml" - name: "Upload coverage file" uses: "actions/upload-artifact@v3" @@ -169,13 +170,14 @@ jobs: - "8.3" oracle-version: - "21" + - "23" include: - php-version: "7.4" oracle-version: "11" services: oracle: - image: gvenzl/oracle-xe:${{ matrix.oracle-version }} + image: gvenzl/oracle-${{ matrix.oracle-version < 23 && 'xe' || 'free' }}:${{ matrix.oracle-version }} env: ORACLE_PASSWORD: oracle ports: @@ -206,7 +208,7 @@ jobs: composer-options: "--ignore-platform-req=php+" - name: "Run PHPUnit" - run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_oci.xml --coverage-clover=coverage.xml" + run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_oci${{ matrix.oracle-version < 23 && '-21' || '' }}.xml --coverage-clover=coverage.xml" - name: "Upload coverage file" uses: "actions/upload-artifact@v3" diff --git a/ci/github/phpunit/oci8-21.xml b/ci/github/phpunit/oci8-21.xml new file mode 100644 index 00000000000..d8e1d99fbf7 --- /dev/null +++ b/ci/github/phpunit/oci8-21.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + ../../../tests + + + + + + ../../../src + + + diff --git a/ci/github/phpunit/oci8.xml b/ci/github/phpunit/oci8.xml index d8e1d99fbf7..75bc15a0268 100644 --- a/ci/github/phpunit/oci8.xml +++ b/ci/github/phpunit/oci8.xml @@ -15,14 +15,14 @@ - + - + diff --git a/ci/github/phpunit/pdo_oci-21.xml b/ci/github/phpunit/pdo_oci-21.xml new file mode 100644 index 00000000000..ef1e272e5e0 --- /dev/null +++ b/ci/github/phpunit/pdo_oci-21.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + ../../../tests + + + + + + ../../../src + + + diff --git a/ci/github/phpunit/pdo_oci.xml b/ci/github/phpunit/pdo_oci.xml index ef1e272e5e0..02da003fcb8 100644 --- a/ci/github/phpunit/pdo_oci.xml +++ b/ci/github/phpunit/pdo_oci.xml @@ -15,14 +15,14 @@ - + - + From ca9fba375bdd8afce2cf03b93e6311de38f16d67 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 14 Oct 2023 15:03:23 -0700 Subject: [PATCH 5/7] Remove unused unit test fixtures --- .../Driver/VersionAwarePlatformDriverTest.php | 72 ++++++------------- 1 file changed, 23 insertions(+), 49 deletions(-) diff --git a/tests/Driver/VersionAwarePlatformDriverTest.php b/tests/Driver/VersionAwarePlatformDriverTest.php index 85fe30c3c99..39f1a505cec 100644 --- a/tests/Driver/VersionAwarePlatformDriverTest.php +++ b/tests/Driver/VersionAwarePlatformDriverTest.php @@ -37,55 +37,29 @@ public function testPDOMySQL(string $version, string $expectedClass): void $this->assertDriverInstantiatesDatabasePlatform(new Driver\PDO\MySQL\Driver(), $version, $expectedClass); } - /** @return array, 2?: string, 3?: bool}> */ + /** @return array}> */ public static function mySQLVersionProvider(): array { return [ - ['5.6.9', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['5.7', MySQL57Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true], - ['5.7.0', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['5.7.8', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['5.7.9', MySQL57Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['5.7.10', MySQL57Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['8', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true], - ['8.0', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true], - ['8.0.11', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false], + ['5.6.9', MySQLPlatform::class], + ['5.7', MySQL57Platform::class], + ['5.7.0', MySQLPlatform::class], + ['5.7.8', MySQLPlatform::class], + ['5.7.9', MySQL57Platform::class], + ['5.7.10', MySQL57Platform::class], + ['8', MySQL80Platform::class], + ['8.0', MySQL80Platform::class], + ['8.0.11', MySQL80Platform::class], ['6', MySQL57Platform::class], - ['10.0.15-MariaDB-1~wheezy', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['5.5.5-10.1.25-MariaDB', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['10.1.2a-MariaDB-a1~lenny-log', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - ['5.5.40-MariaDB-1~wheezy', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false], - [ - '5.5.5-MariaDB-10.2.8+maria~xenial-log', - MariaDb1027Platform::class, - 'https://github.com/doctrine/dbal/pull/5779', - false, - ], - [ - '10.2.8-MariaDB-10.2.8+maria~xenial-log', - MariaDb1027Platform::class, - 'https://github.com/doctrine/dbal/pull/5779', - false, - ], - [ - '10.2.8-MariaDB-1~lenny-log', - MariaDb1027Platform::class, - 'https://github.com/doctrine/dbal/pull/5779', - false, - ], - ['mariadb-10.9.3', MariaDB1052Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true], - [ - '10.5.2-MariaDB-1~lenny-log', - MariaDB1052Platform::class, - 'https://github.com/doctrine/dbal/pull/5779', - false, - ], - [ - '11.0.2-MariaDB-1:11.0.2+maria~ubu2204', - MariaDB1052Platform::class, - 'https://github.com/doctrine/dbal/pull/5779', - false, - ], + ['10.0.15-MariaDB-1~wheezy', MySQLPlatform::class], + ['5.5.5-10.1.25-MariaDB', MySQLPlatform::class], + ['10.1.2a-MariaDB-a1~lenny-log', MySQLPlatform::class], + ['5.5.40-MariaDB-1~wheezy', MySQLPlatform::class], + ['5.5.5-MariaDB-10.2.8+maria~xenial-log', MariaDb1027Platform::class], + ['10.2.8-MariaDB-10.2.8+maria~xenial-log', MariaDb1027Platform::class], + ['10.2.8-MariaDB-1~lenny-log', MariaDb1027Platform::class], + ['10.5.2-MariaDB-1~lenny-log', MariaDB1052Platform::class], + ['11.0.2-MariaDB-1:11.0.2+maria~ubu2204', MariaDB1052Platform::class], ]; } @@ -118,13 +92,13 @@ public function testIBMDB2(string $version, string $expectedClass): void $this->assertDriverInstantiatesDatabasePlatform(new Driver\IBMDB2\Driver(), $version, $expectedClass); } - /** @return array, 2?: string, 3?: bool}> */ + /** @return array}> */ public static function db2VersionProvider(): array { return [ - ['10.1.0', DB2Platform::class, 'https://github.com/doctrine/dbal/pull/5156', true], - ['10.1.0.0', DB2Platform::class, 'https://github.com/doctrine/dbal/pull/5156', true], - ['DB2/LINUXX8664 10.1.0.0', DB2Platform::class, 'https://github.com/doctrine/dbal/pull/5156', true], + ['10.1.0', DB2Platform::class], + ['10.1.0.0', DB2Platform::class], + ['DB2/LINUXX8664 10.1.0.0', DB2Platform::class], ['11.1.0', DB2111Platform::class], ['11.1.0.0', DB2111Platform::class], ['DB2/LINUXX8664 11.1.0.0', DB2111Platform::class], From c32107ba19e16bf21090b18a4e9d0d94ac5d7382 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 14 Oct 2023 16:08:09 -0700 Subject: [PATCH 6/7] Fix coding standard violation --- src/Driver/OCI8/Driver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Driver/OCI8/Driver.php b/src/Driver/OCI8/Driver.php index fe091bfa982..650a4f9ab34 100644 --- a/src/Driver/OCI8/Driver.php +++ b/src/Driver/OCI8/Driver.php @@ -8,6 +8,7 @@ use SensitiveParameter; use function oci_connect; +use function oci_new_connect; use function oci_pconnect; use const OCI_NO_AUTO_COMMIT; From 24735b4bd3dd02223839350f1a31a26759bcd05a Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 14 Oct 2023 13:56:15 -0700 Subject: [PATCH 7/7] Enable skipping locked rows in QueryBuilder Co-authored-by: Herberto Graca --- UPGRADE.md | 5 + psalm.xml.dist | 3 + src/Driver/AbstractMySQLDriver.php | 5 + src/Id/TableGenerator.php | 13 +- src/Platforms/AbstractMySQLPlatform.php | 7 + src/Platforms/AbstractPlatform.php | 34 +++++ src/Platforms/DB2Platform.php | 9 ++ src/Platforms/MariaDb1060Platform.php | 16 +++ src/Platforms/MySQL80Platform.php | 6 + src/Platforms/PostgreSQL100Platform.php | 6 + src/Platforms/PostgreSQLPlatform.php | 7 + .../SQL/Builder/SQLServerSelectSQLBuilder.php | 86 +++++++++++ src/Platforms/SQLServerPlatform.php | 9 ++ src/Platforms/SqlitePlatform.php | 10 ++ src/Query/ForUpdate.php | 21 +++ .../ForUpdate/ConflictResolutionMode.php | 27 ++++ src/Query/Limit.php | 30 ++++ src/Query/QueryBuilder.php | 74 +++++----- src/Query/SelectQuery.php | 107 ++++++++++++++ src/SQL/Builder/DefaultSelectSQLBuilder.php | 95 +++++++++++++ src/SQL/Builder/SelectSQLBuilder.php | 12 ++ .../Driver/VersionAwarePlatformDriverTest.php | 5 +- tests/Functional/Query/QueryBuilderTest.php | 134 ++++++++++++++++++ tests/Query/QueryBuilderTest.php | 14 +- 24 files changed, 692 insertions(+), 43 deletions(-) create mode 100644 src/Platforms/MariaDb1060Platform.php create mode 100644 src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php create mode 100644 src/Query/ForUpdate.php create mode 100644 src/Query/ForUpdate/ConflictResolutionMode.php create mode 100644 src/Query/Limit.php create mode 100644 src/Query/SelectQuery.php create mode 100644 src/SQL/Builder/DefaultSelectSQLBuilder.php create mode 100644 src/SQL/Builder/SelectSQLBuilder.php create mode 100644 tests/Functional/Query/QueryBuilderTest.php diff --git a/UPGRADE.md b/UPGRADE.md index 06e70fdcd3c..73282a807a4 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,6 +8,11 @@ awareness about deprecated code. # Upgrade to 3.8 +## Deprecated lock-related `AbstractPlatform` methods + +The usage of `AbstractPlatform::getReadLockSQL()`, `::getWriteLockSQL()` and `::getForUpdateSQL` is deprecated as this +API is not portable. Use `QueryBuilder::forUpdate()` as a replacement for the latter. + ## Deprecated `AbstractMySQLPlatform` methods * `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` has been deprecated diff --git a/psalm.xml.dist b/psalm.xml.dist index 0eb1e5ced86..ba2e28f574d 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -515,6 +515,9 @@ + + + diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index 5f6ffd6f5f9..83159a540da 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -11,6 +11,7 @@ use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MariaDb1043Platform; use Doctrine\DBAL\Platforms\MariaDb1052Platform; +use Doctrine\DBAL\Platforms\MariaDb1060Platform; use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; @@ -39,6 +40,10 @@ public function createDatabasePlatformForVersion($version) if ($mariadb) { $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); + if (version_compare($mariaDbVersion, '10.6.0', '>=')) { + return new MariaDb1060Platform(); + } + if (version_compare($mariaDbVersion, '10.5.2', '>=')) { return new MariaDb1052Platform(); } diff --git a/src/Id/TableGenerator.php b/src/Id/TableGenerator.php index 7d7f210e219..51e541d54ff 100644 --- a/src/Id/TableGenerator.php +++ b/src/Id/TableGenerator.php @@ -6,7 +6,6 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Exception; -use Doctrine\DBAL\LockMode; use Doctrine\Deprecations\Deprecation; use Throwable; @@ -115,11 +114,13 @@ public function nextValue($sequence) $this->conn->beginTransaction(); try { - $platform = $this->conn->getDatabasePlatform(); - $sql = 'SELECT sequence_value, sequence_increment_by' - . ' FROM ' . $platform->appendLockHint($this->generatorTableName, LockMode::PESSIMISTIC_WRITE) - . ' WHERE sequence_name = ? ' . $platform->getWriteLockSQL(); - $row = $this->conn->fetchAssociative($sql, [$sequence]); + $row = $this->conn->createQueryBuilder() + ->select('sequence_value', 'sequence_increment_by') + ->from($this->generatorTableName) + ->where('sequence_name = ?') + ->forUpdate() + ->setParameter(1, $sequence) + ->fetchAssociative(); if ($row !== false) { $row = array_change_key_case($row, CASE_LOWER); diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index bc409bdf4cb..8651b82783d 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -11,6 +11,8 @@ use Doctrine\DBAL\Schema\MySQLSchemaManager; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder; +use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\TransactionIsolationLevel; use Doctrine\DBAL\Types\BlobType; use Doctrine\DBAL\Types\TextType; @@ -541,6 +543,11 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] return $sql; } + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); + } + /** * {@inheritDoc} * diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index eba3aefc890..c77882a1a53 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -29,6 +29,8 @@ use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\TableDiff; use Doctrine\DBAL\Schema\UniqueConstraint; +use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder; +use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\SQL\Parser; use Doctrine\DBAL\TransactionIsolationLevel; use Doctrine\DBAL\Types; @@ -1758,10 +1760,19 @@ abstract public function getCurrentDatabaseExpression(): string; /** * Returns the FOR UPDATE expression. * + * @deprecated This API is not portable. Use {@link QueryBuilder::forUpdate()}` instead. + * * @return string */ public function getForUpdateSQL() { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + return 'FOR UPDATE'; } @@ -1793,10 +1804,19 @@ public function appendLockHint(string $fromClause, int $lockMode): string * This defaults to the ANSI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database * vendors allow to lighten this constraint up to be a real read lock. * + * @deprecated This API is not portable. + * * @return string */ public function getReadLockSQL() { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + return $this->getForUpdateSQL(); } @@ -1805,10 +1825,19 @@ public function getReadLockSQL() * * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ANSI SQL standard. * + * @deprecated This API is not portable. + * * @return string */ public function getWriteLockSQL() { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + return $this->getForUpdateSQL(); } @@ -2052,6 +2081,11 @@ public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDE ); } + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED'); + } + /** * @internal * diff --git a/src/Platforms/DB2Platform.php b/src/Platforms/DB2Platform.php index b203ce8a052..64fabc32d0a 100644 --- a/src/Platforms/DB2Platform.php +++ b/src/Platforms/DB2Platform.php @@ -9,6 +9,8 @@ use Doctrine\DBAL\Schema\Identifier; use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder; +use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\Deprecations\Deprecation; @@ -974,8 +976,15 @@ public function prefersIdentityColumns() return true; } + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); + } + /** * {@inheritDoc} + * + * @deprecated This API is not portable. */ public function getForUpdateSQL() { diff --git a/src/Platforms/MariaDb1060Platform.php b/src/Platforms/MariaDb1060Platform.php new file mode 100644 index 00000000000..82d11f21d41 --- /dev/null +++ b/src/Platforms/MariaDb1060Platform.php @@ -0,0 +1,16 @@ +platform = $platform; + } + + public function buildSQL(SelectQuery $query): string + { + $parts = ['SELECT']; + + if ($query->isDistinct()) { + $parts[] = 'DISTINCT'; + } + + $parts[] = implode(', ', $query->getColumns()); + + $from = $query->getFrom(); + + if (count($from) > 0) { + $parts[] = 'FROM ' . implode(', ', $from); + } + + $forUpdate = $query->getForUpdate(); + + if ($forUpdate !== null) { + $with = ['UPDLOCK', 'ROWLOCK']; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + $with[] = 'READPAST'; + } + + $parts[] = 'WITH (' . implode(', ', $with) . ')'; + } + + $where = $query->getWhere(); + + if ($where !== null) { + $parts[] = 'WHERE ' . $where; + } + + $groupBy = $query->getGroupBy(); + + if (count($groupBy) > 0) { + $parts[] = 'GROUP BY ' . implode(', ', $groupBy); + } + + $having = $query->getHaving(); + + if ($having !== null) { + $parts[] = 'HAVING ' . $having; + } + + $orderBy = $query->getOrderBy(); + + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + return $sql; + } +} diff --git a/src/Platforms/SQLServerPlatform.php b/src/Platforms/SQLServerPlatform.php index 113055ba896..c8cd20b561c 100644 --- a/src/Platforms/SQLServerPlatform.php +++ b/src/Platforms/SQLServerPlatform.php @@ -5,6 +5,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\InvalidLockMode; use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Platforms\SQLServer\SQL\Builder\SQLServerSelectSQLBuilder; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\ColumnDiff; use Doctrine\DBAL\Schema\ForeignKeyConstraint; @@ -14,6 +15,7 @@ use Doctrine\DBAL\Schema\SQLServerSchemaManager; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\Types\Types; use Doctrine\Deprecations\Deprecation; use InvalidArgumentException; @@ -49,6 +51,11 @@ */ class SQLServerPlatform extends AbstractPlatform { + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new SQLServerSelectSQLBuilder($this); + } + /** * {@inheritDoc} */ @@ -1610,6 +1617,8 @@ public function appendLockHint(string $fromClause, int $lockMode): string /** * {@inheritDoc} + * + * @deprecated This API is not portable. */ public function getForUpdateSQL() { diff --git a/src/Platforms/SqlitePlatform.php b/src/Platforms/SqlitePlatform.php index 5acefc5c82f..d951662ae10 100644 --- a/src/Platforms/SqlitePlatform.php +++ b/src/Platforms/SqlitePlatform.php @@ -14,6 +14,8 @@ use Doctrine\DBAL\Schema\SqliteSchemaManager; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder; +use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\TransactionIsolationLevel; use Doctrine\DBAL\Types; use Doctrine\DBAL\Types\IntegerType; @@ -193,6 +195,12 @@ public function getCurrentDatabaseExpression(): string return "'main'"; } + /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */ + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, null, null); + } + /** * {@inheritDoc} */ @@ -734,6 +742,8 @@ public static function udfLocate($str, $substr, $offset = 0) /** * {@inheritDoc} + * + * @deprecated This API is not portable. */ public function getForUpdateSQL() { diff --git a/src/Query/ForUpdate.php b/src/Query/ForUpdate.php new file mode 100644 index 00000000000..fe54df909ee --- /dev/null +++ b/src/Query/ForUpdate.php @@ -0,0 +1,21 @@ +conflictResolutionMode = $conflictResolutionMode; + } + + public function getConflictResolutionMode(): int + { + return $this->conflictResolutionMode; + } +} diff --git a/src/Query/ForUpdate/ConflictResolutionMode.php b/src/Query/ForUpdate/ConflictResolutionMode.php new file mode 100644 index 00000000000..f968f7b941d --- /dev/null +++ b/src/Query/ForUpdate/ConflictResolutionMode.php @@ -0,0 +1,27 @@ +maxResults = $maxResults; + $this->firstResult = $firstResult; + } + + public function isDefined(): bool + { + return $this->maxResults !== null || $this->firstResult !== 0; + } + + public function getMaxResults(): ?int + { + return $this->maxResults; + } + + public function getFirstResult(): int + { + return $this->firstResult; + } +} diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php index c4d67cf5d39..6799c12c5e2 100644 --- a/src/Query/QueryBuilder.php +++ b/src/Query/QueryBuilder.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Query\Expression\CompositeExpression; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; +use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode; use Doctrine\DBAL\Result; use Doctrine\DBAL\Statement; use Doctrine\DBAL\Types\Type; @@ -70,16 +71,17 @@ class QueryBuilder * The default values of SQL parts collection */ private const SQL_PARTS_DEFAULTS = [ - 'select' => [], - 'distinct' => false, - 'from' => [], - 'join' => [], - 'set' => [], - 'where' => null, - 'groupBy' => [], - 'having' => null, - 'orderBy' => [], - 'values' => [], + 'select' => [], + 'distinct' => false, + 'from' => [], + 'join' => [], + 'set' => [], + 'where' => null, + 'groupBy' => [], + 'having' => null, + 'orderBy' => [], + 'values' => [], + 'for_update' => null, ]; /** @@ -591,6 +593,20 @@ public function getMaxResults() return $this->maxResults; } + /** + * Locks the queried rows for a subsequent update. + * + * @return $this + */ + public function forUpdate(int $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self + { + $this->state = self::STATE_DIRTY; + + $this->sqlParts['for_update'] = new ForUpdate($conflictResolutionMode); + + return $this; + } + /** * Either appends to or replaces a single, generic query part. * @@ -1475,27 +1491,24 @@ public function resetOrderBy(): self return $this; } - /** @throws QueryException */ + /** @throws Exception */ private function getSQLForSelect(): string { - $query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') . - implode(', ', $this->sqlParts['select']); - - $query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '') - . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '') - . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '') - . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '') - . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : ''); - - if ($this->isLimitQuery()) { - return $this->connection->getDatabasePlatform()->modifyLimitQuery( - $query, - $this->maxResults, - $this->firstResult, + return $this->connection->getDatabasePlatform() + ->createSelectSQLBuilder() + ->buildSQL( + new SelectQuery( + $this->sqlParts['distinct'], + $this->sqlParts['select'], + $this->getFromClauses(), + $this->sqlParts['where'], + $this->sqlParts['groupBy'], + $this->sqlParts['having'], + $this->sqlParts['orderBy'], + new Limit($this->maxResults, $this->firstResult), + $this->sqlParts['for_update'], + ), ); - } - - return $query; } /** @@ -1542,11 +1555,6 @@ private function verifyAllAliasesAreKnown(array $knownAliases): void } } - private function isLimitQuery(): bool - { - return $this->maxResults !== null || $this->firstResult !== 0; - } - /** * Converts this instance into an INSERT string in SQL. */ diff --git a/src/Query/SelectQuery.php b/src/Query/SelectQuery.php new file mode 100644 index 00000000000..ed9a7bc2dfb --- /dev/null +++ b/src/Query/SelectQuery.php @@ -0,0 +1,107 @@ +distinct = $distinct; + $this->columns = $columns; + $this->from = $from; + $this->where = $where; + $this->groupBy = $groupBy; + $this->having = $having; + $this->orderBy = $orderBy; + $this->limit = $limit; + $this->forUpdate = $forUpdate; + } + + public function isDistinct(): bool + { + return $this->distinct; + } + + /** @return string[] */ + public function getColumns(): array + { + return $this->columns; + } + + /** @return string[] */ + public function getFrom(): array + { + return $this->from; + } + + public function getWhere(): ?string + { + return $this->where; + } + + /** @return string[] */ + public function getGroupBy(): array + { + return $this->groupBy; + } + + public function getHaving(): ?string + { + return $this->having; + } + + /** @return string[] */ + public function getOrderBy(): array + { + return $this->orderBy; + } + + public function getLimit(): Limit + { + return $this->limit; + } + + public function getForUpdate(): ?ForUpdate + { + return $this->forUpdate; + } +} diff --git a/src/SQL/Builder/DefaultSelectSQLBuilder.php b/src/SQL/Builder/DefaultSelectSQLBuilder.php new file mode 100644 index 00000000000..9dcb7a197fa --- /dev/null +++ b/src/SQL/Builder/DefaultSelectSQLBuilder.php @@ -0,0 +1,95 @@ +platform = $platform; + $this->forUpdateSQL = $forUpdateSQL; + $this->skipLockedSQL = $skipLockedSQL; + } + + /** @throws Exception */ + public function buildSQL(SelectQuery $query): string + { + $parts = ['SELECT']; + + if ($query->isDistinct()) { + $parts[] = 'DISTINCT'; + } + + $parts[] = implode(', ', $query->getColumns()); + + $from = $query->getFrom(); + + if (count($from) > 0) { + $parts[] = 'FROM ' . implode(', ', $from); + } + + $where = $query->getWhere(); + + if ($where !== null) { + $parts[] = 'WHERE ' . $where; + } + + $groupBy = $query->getGroupBy(); + + if (count($groupBy) > 0) { + $parts[] = 'GROUP BY ' . implode(', ', $groupBy); + } + + $having = $query->getHaving(); + + if ($having !== null) { + $parts[] = 'HAVING ' . $having; + } + + $orderBy = $query->getOrderBy(); + + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + $forUpdate = $query->getForUpdate(); + + if ($forUpdate !== null) { + if ($this->forUpdateSQL === null) { + throw Exception::notSupported('FOR UPDATE'); + } + + $sql .= ' ' . $this->forUpdateSQL; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + if ($this->skipLockedSQL === null) { + throw Exception::notSupported('SKIP LOCKED'); + } + + $sql .= ' ' . $this->skipLockedSQL; + } + } + + return $sql; + } +} diff --git a/src/SQL/Builder/SelectSQLBuilder.php b/src/SQL/Builder/SelectSQLBuilder.php new file mode 100644 index 00000000000..ddbf73c03ba --- /dev/null +++ b/src/SQL/Builder/SelectSQLBuilder.php @@ -0,0 +1,12 @@ +addColumn('id', Types::INTEGER); + $table->setPrimaryKey(['id']); + + $this->dropAndCreateTable($table); + + $this->connection->insert('for_update', ['id' => 1]); + $this->connection->insert('for_update', ['id' => 2]); + } + + protected function tearDown(): void + { + if (! $this->connection->isTransactionActive()) { + return; + } + + $this->connection->rollBack(); + } + + public function testForUpdateOrdinary(): void + { + $platform = $this->connection->getDatabasePlatform(); + + if ($platform instanceof SqlitePlatform) { + self::markTestSkipped('Skipping on SQLite'); + } + + $qb1 = $this->connection->createQueryBuilder(); + $qb1->select('id') + ->from('for_update') + ->forUpdate(); + + self::assertEquals([1, 2], $qb1->fetchFirstColumn()); + } + + public function testForUpdateSkipLockedWhenSupported(): void + { + if (! $this->platformSupportsSkipLocked()) { + self::markTestSkipped('The database platform does not support SKIP LOCKED.'); + } + + $qb1 = $this->connection->createQueryBuilder(); + $qb1->select('id') + ->from('for_update') + ->where('id = 1') + ->forUpdate(); + + $this->connection->beginTransaction(); + + self::assertEquals([1], $qb1->fetchFirstColumn()); + + $params = TestUtil::getConnectionParams(); + + if (TestUtil::isDriverOneOf('oci8')) { + $params['driverOptions']['exclusive'] = true; + } + + $connection2 = DriverManager::getConnection($params); + + $qb2 = $connection2->createQueryBuilder(); + $qb2->select('id') + ->from('for_update') + ->orderBy('id') + ->forUpdate(ConflictResolutionMode::SKIP_LOCKED); + + self::assertEquals([2], $qb2->fetchFirstColumn()); + } + + public function testForUpdateSkipLockedWhenNotSupported(): void + { + if ($this->platformSupportsSkipLocked()) { + self::markTestSkipped('The database platform supports SKIP LOCKED.'); + } + + $qb = $this->connection->createQueryBuilder(); + $qb->select('id') + ->from('for_update') + ->forUpdate(ConflictResolutionMode::SKIP_LOCKED); + + self::expectException(Exception::class); + $qb->executeQuery(); + } + + private function platformSupportsSkipLocked(): bool + { + $platform = $this->connection->getDatabasePlatform(); + + if ($platform instanceof DB2Platform) { + return false; + } + + if ($platform instanceof MySQLPlatform) { + if ($platform instanceof MariaDBPlatform) { + if (! $platform instanceof MariaDb1060Platform) { + return false; + } + } elseif (! $platform instanceof MySQL80Platform) { + return false; + } + } + + if ($platform instanceof PostgreSQLPlatform && ! $platform instanceof PostgreSQL100Platform) { + return false; + } + + return ! $platform instanceof SqlitePlatform; + } +} diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php index ba774b748aa..a9d488bfc05 100644 --- a/tests/Query/QueryBuilderTest.php +++ b/tests/Query/QueryBuilderTest.php @@ -6,10 +6,12 @@ use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Connection; use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Query\QueryException; use Doctrine\DBAL\Result; +use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; @@ -31,9 +33,15 @@ protected function setUp(): void $expressionBuilder = new ExpressionBuilder($this->conn); - $this->conn->expects(self::any()) - ->method('getExpressionBuilder') - ->willReturn($expressionBuilder); + $this->conn->method('getExpressionBuilder') + ->willReturn($expressionBuilder); + + $platform = $this->createMock(AbstractPlatform::class); + $platform->method('createSelectSQLBuilder') + ->willReturn(new DefaultSelectSQLBuilder($platform, null, null)); + + $this->conn->method('getDatabasePlatform') + ->willReturn($platform); } public function testSimpleSelectWithoutFrom(): void