diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index a065b589bff..5963083840a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -106,10 +106,11 @@ jobs: oracle-version: - "18" - "21" + - "23" 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: @@ -140,7 +141,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" @@ -162,10 +163,11 @@ jobs: oracle-version: - "18" - "21" + - "23" 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: @@ -196,7 +198,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/UPGRADE.md b/UPGRADE.md index 6662e215a47..8fb7172b20a 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -921,6 +921,18 @@ The following methods have been removed. # 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 + 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` `QueryBuilder::resetQueryParts()` has been deprecated. 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 f333e9d30a8..c13b34d055b 100644 --- a/ci/github/phpunit/oci8.xml +++ b/ci/github/phpunit/oci8.xml @@ -14,14 +14,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 7a9fb1ee900..3a83d2e4d03 100644 --- a/ci/github/phpunit/pdo_oci.xml +++ b/ci/github/phpunit/pdo_oci.xml @@ -14,14 +14,14 @@ - + - + diff --git a/psalm.xml.dist b/psalm.xml.dist index fa949b53ddd..c15dddaf5b7 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -44,8 +44,19 @@ + + + + + + + + diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index ce58f9aa36b..8d81b6c0e16 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion; use Doctrine\DBAL\Platforms\MariaDB1052Platform; +use Doctrine\DBAL\Platforms\MariaDB1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; @@ -33,6 +34,10 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs $version = $versionProvider->getServerVersion(); if (stripos($version, 'mariadb') !== false) { $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/Driver/OCI8/Driver.php b/src/Driver/OCI8/Driver.php index 698cf359a42..d519cc94c75 100644 --- a/src/Driver/OCI8/Driver.php +++ b/src/Driver/OCI8/Driver.php @@ -10,6 +10,7 @@ use SensitiveParameter; use function oci_connect; +use function oci_new_connect; use function oci_pconnect; use const OCI_NO_AUTO_COMMIT; diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index d3f411a83d6..0a0ad9ad6a7 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -14,13 +14,17 @@ use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\MySQLSchemaManager; 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\Types; +use Doctrine\Deprecations\Deprecation; use function array_merge; use function array_unique; use function array_values; use function count; +use function func_get_args; use function implode; use function in_array; use function is_numeric; @@ -216,6 +220,8 @@ public function supportsColumnCollation(): bool } /** + * @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] @@ -224,7 +230,24 @@ public function supportsColumnCollation(): bool */ public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array { - return [$tableAlias . '.COLUMN_TYPE', '']; + 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()), '']; + } + + /** + * The SQL snippet required to elucidate a column type + * + * Returns a column type SELECT snippet string + */ + public function getColumnTypeSQLSnippet(string $tableAlias, string $databaseName): string + { + return $tableAlias . '.COLUMN_TYPE'; } /** @@ -282,6 +305,11 @@ protected function _getCreateTableSQL(string $name, array $columns, array $optio return $sql; } + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); + } + /** * Build SQL for table options * diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index 51363f46bd9..6dcf3ba0ede 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -26,11 +26,14 @@ 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; use Doctrine\DBAL\Types\Exception\TypeNotFound; use Doctrine\DBAL\Types\Type; +use Doctrine\Deprecations\Deprecation; use function addcslashes; use function array_map; @@ -700,9 +703,18 @@ abstract public function getCurrentDatabaseExpression(): string; /** * Returns the FOR UPDATE expression. + * + * @deprecated This API is not portable. Use {@link QueryBuilder::forUpdate()}` instead. */ public function getForUpdateSQL(): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + return 'FOR UPDATE'; } @@ -722,9 +734,18 @@ public function appendLockHint(string $fromClause, LockMode $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. */ public function getReadLockSQL(): string { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + return $this->getForUpdateSQL(); } @@ -732,9 +753,18 @@ public function getReadLockSQL(): string * Returns the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows. * * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ANSI SQL standard. + * + * @deprecated This API is not portable. */ public function getWriteLockSQL(): string { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + return $this->getForUpdateSQL(); } @@ -799,6 +829,11 @@ public function getCreateTableSQL(Table $table): array return $this->buildCreateTableSQL($table, true); } + 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 6e894b5e59e..31cdfb677b7 100644 --- a/src/Platforms/DB2Platform.php +++ b/src/Platforms/DB2Platform.php @@ -13,6 +13,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\TransactionIsolationLevel; use Doctrine\DBAL\Types\Types; @@ -557,6 +559,12 @@ public function supportsIdentityColumns(): bool return true; } + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); + } + + /** @deprecated This API is not portable. */ public function getForUpdateSQL(): string { return ' WITH RR USE AND KEEP UPDATE LOCKS'; diff --git a/src/Platforms/MariaDB1060Platform.php b/src/Platforms/MariaDB1060Platform.php new file mode 100644 index 00000000000..6f45f1021fa --- /dev/null +++ b/src/Platforms/MariaDB1060Platform.php @@ -0,0 +1,18 @@ +quoteStringLiteral($databaseName); + + // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues + return <<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 ee4d217b99d..6078fe7baad 100644 --- a/src/Platforms/SQLServerPlatform.php +++ b/src/Platforms/SQLServerPlatform.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\LockMode; use Doctrine\DBAL\Platforms\Keywords\KeywordList; use Doctrine\DBAL\Platforms\Keywords\SQLServerKeywords; +use Doctrine\DBAL\Platforms\SQLServer\SQL\Builder\SQLServerSelectSQLBuilder; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\ColumnDiff; use Doctrine\DBAL\Schema\Identifier; @@ -16,6 +17,7 @@ use Doctrine\DBAL\Schema\Sequence; use Doctrine\DBAL\Schema\SQLServerSchemaManager; use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\TransactionIsolationLevel; use Doctrine\DBAL\Types\Types; use InvalidArgumentException; @@ -47,6 +49,11 @@ class SQLServerPlatform extends AbstractPlatform /** @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy. */ public const OPTION_DEFAULT_CONSTRAINT_NAME = 'default_constraint_name'; + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new SQLServerSelectSQLBuilder($this); + } + public function getCurrentDateSQL(): string { return $this->getConvertExpression('date', 'GETDATE()'); @@ -1089,6 +1096,7 @@ public function appendLockHint(string $fromClause, LockMode $lockMode): string }; } + /** @deprecated This API is not portable. */ public function getForUpdateSQL(): string { return ' '; diff --git a/src/Platforms/SQLitePlatform.php b/src/Platforms/SQLitePlatform.php index f2aa46a0485..0cca9c67eb6 100644 --- a/src/Platforms/SQLitePlatform.php +++ b/src/Platforms/SQLitePlatform.php @@ -17,6 +17,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; @@ -147,6 +149,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); + } + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string { return match ($level) { diff --git a/src/Query/ForUpdate.php b/src/Query/ForUpdate.php new file mode 100644 index 00000000000..f511f6404be --- /dev/null +++ b/src/Query/ForUpdate.php @@ -0,0 +1,19 @@ +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 !== 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 435c861c6c7..a2710592f52 100644 --- a/src/Query/QueryBuilder.php +++ b/src/Query/QueryBuilder.php @@ -13,6 +13,7 @@ use Doctrine\DBAL\Query\Exception\UnknownAlias; 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; @@ -143,6 +144,8 @@ class QueryBuilder */ private array $orderBy = []; + private ?ForUpdate $forUpdate = null; + /** * The values of an INSERT query. * @@ -484,6 +487,20 @@ public function getMaxResults(): ?int return $this->maxResults; } + /** + * Locks the queried rows for a subsequent update. + * + * @return $this + */ + public function forUpdate(int $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self + { + $this->forUpdate = new ForUpdate($conflictResolutionMode); + + $this->sql = null; + + return $this; + } + /** * Specifies an item that is to be returned in the query result. * Replaces any previously specified selections, if any. @@ -503,10 +520,6 @@ public function select(string ...$expressions): self { $this->type = QueryType::SELECT; - if (count($expressions) < 1) { - return $this; - } - $this->select = $expressions; $this->sql = null; @@ -1190,50 +1203,28 @@ public function resetOrderBy(): self return $this; } - /** @throws QueryException */ + /** @throws Exception */ private function getSQLForSelect(): string { if (count($this->select) === 0) { throw new QueryException('No SELECT expressions given. Please use select() or addSelect().'); } - $query = 'SELECT'; - - if ($this->distinct) { - $query .= ' DISTINCT'; - } - - $query .= ' ' . implode(', ', $this->select); - - if (count($this->from) !== 0) { - $query .= ' FROM ' . implode(', ', $this->getFromClauses()); - } - - if ($this->where !== null) { - $query .= ' WHERE ' . $this->where; - } - - if (count($this->groupBy) !== 0) { - $query .= ' GROUP BY ' . implode(', ', $this->groupBy); - } - - if ($this->having !== null) { - $query .= ' HAVING ' . $this->having; - } - - if (count($this->orderBy) !== 0) { - $query .= ' ORDER BY ' . implode(', ', $this->orderBy); - } - - if ($this->isLimitQuery()) { - return $this->connection->getDatabasePlatform()->modifyLimitQuery( - $query, - $this->maxResults, - $this->firstResult, + return $this->connection->getDatabasePlatform() + ->createSelectSQLBuilder() + ->buildSQL( + new SelectQuery( + $this->distinct, + $this->select, + $this->getFromClauses(), + $this->where !== null ? (string) $this->where : null, + $this->groupBy, + $this->having !== null ? (string) $this->having : null, + $this->orderBy, + new Limit($this->maxResults, $this->firstResult), + $this->forUpdate, + ), ); - } - - return $query; } /** @@ -1279,11 +1270,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..e6ef9f26c24 --- /dev/null +++ b/src/Query/SelectQuery.php @@ -0,0 +1,78 @@ +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..a30120e5eb2 --- /dev/null +++ b/src/SQL/Builder/DefaultSelectSQLBuilder.php @@ -0,0 +1,94 @@ +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 NotSupported::new('FOR UPDATE'); + } + + $sql .= ' ' . $this->forUpdateSQL; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + if ($this->skipLockedSQL === null) { + throw NotSupported::new('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..c013f96a8bd --- /dev/null +++ b/src/SQL/Builder/SelectSQLBuilder.php @@ -0,0 +1,14 @@ +platform->getColumnTypeSQLSnippets(); + // @todo 4.0 - call getColumnTypeSQLSnippet() instead + [$columnTypeSQL, $joinCheckConstraintSQL] = $this->platform->getColumnTypeSQLSnippets('c', $databaseName); $sql = 'SELECT'; diff --git a/tests/Driver/VersionAwarePlatformDriverTest.php b/tests/Driver/VersionAwarePlatformDriverTest.php index 55c830bedd3..f6b8c142494 100644 --- a/tests/Driver/VersionAwarePlatformDriverTest.php +++ b/tests/Driver/VersionAwarePlatformDriverTest.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDB1052Platform; +use Doctrine\DBAL\Platforms\MariaDB1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; @@ -39,7 +40,8 @@ public static function mySQLVersionProvider(): array ['10.2.8-MariaDB-10.2.8+maria~xenial-log', MariaDBPlatform::class], ['10.2.8-MariaDB-1~lenny-log', MariaDBPlatform::class], ['10.5.2-MariaDB-1~lenny-log', MariaDB1052Platform::class], - ['11.0.2-MariaDB-1:11.0.2+maria~ubu2204', MariaDB1052Platform::class], + ['10.6.0-MariaDB-1~lenny-log', MariaDB1060Platform::class], + ['11.0.2-MariaDB-1:11.0.2+maria~ubu2204', MariaDB1060Platform::class], ]; } diff --git a/tests/Functional/Query/QueryBuilderTest.php b/tests/Functional/Query/QueryBuilderTest.php new file mode 100644 index 00000000000..cc047ac44bf --- /dev/null +++ b/tests/Functional/Query/QueryBuilderTest.php @@ -0,0 +1,126 @@ +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) { + return $platform instanceof MySQL80Platform; + } + + if ($platform instanceof MariaDBPlatform) { + return $platform instanceof MariaDB1060Platform; + } + + return ! $platform instanceof SQLitePlatform; + } +} diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index a167407aecd..2b17b6a25be 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Tests\Functional\Schema\MySQL\PointType; use Doctrine\DBAL\Tests\TestUtil; use Doctrine\DBAL\Types\BlobType; +use Doctrine\DBAL\Types\JsonType; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; @@ -387,6 +388,17 @@ public function testListFloatTypeColumns(): void self::assertTrue($columns['col_unsigned']->getUnsigned()); } + public function testJsonColumnType(): void + { + $table = new Table('test_mysql_json'); + $table->addColumn('col_json', Types::JSON); + $this->dropAndCreateTable($table); + + $columns = $this->schemaManager->listTableColumns('test_mysql_json'); + + self::assertInstanceOf(JsonType::class, $columns['col_json']->getType()); + } + public function testColumnDefaultCurrentTimestamp(): void { $platform = $this->connection->getDatabasePlatform(); diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php index 5e9f562259a..8e287b9ba45 100644 --- a/tests/Query/QueryBuilderTest.php +++ b/tests/Query/QueryBuilderTest.php @@ -8,10 +8,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\Types; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -30,9 +32,15 @@ protected function setUp(): void $expressionBuilder = new ExpressionBuilder($this->conn); - $this->conn->expects(self::any()) - ->method('createExpressionBuilder') - ->willReturn($expressionBuilder); + $this->conn->method('createExpressionBuilder') + ->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