diff --git a/UPGRADE.md b/UPGRADE.md index 35bea055b68..d47516fbefd 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,11 @@ # Upgrade to 3.0 +## BC BREAK: Changes in driver-level exception handling + +1. The `convertException()` method has been removed from the `Driver` interface. The logic of exception conversion has been moved to the `ExceptionConverter` interface. The drivers now must implement the `getExceptionConverter()` method. +2. The `driverException()` and `driverExceptionDuringQuery()` factory methods have been removed from the `DBALException` class. +3. Non-driver exceptions (e.g. exceptions of type `Error`) are no longer wrapped in a `DBALException`. + ## BC BREAK: More driver-level methods are allowed to throw a Driver\Exception. The following driver-level methods are allowed to throw a Driver\Exception: diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9e4cfe7528c..57c383512ab 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,10 +8,6 @@ parameters: reportUnmatchedIgnoredErrors: false checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false - earlyTerminatingMethodCalls: - Doctrine\DBAL\Connection: - - handleDriverException - - handleExceptionDuringQuery ignoreErrors: # removing it would be BC break - '~^Constructor of class Doctrine\\DBAL\\Schema\\Table has an unused parameter \$idGeneratorType\.\z~' diff --git a/src/Connection.php b/src/Connection.php index d60d71a07ee..99c111d4fde 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Cache\CacheException; use Doctrine\DBAL\Cache\CachingResult; use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Driver\Connection as DriverConnection; use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Driver\Result as DriverResult; @@ -26,12 +27,18 @@ use Traversable; use function array_key_exists; +use function array_map; use function assert; +use function bin2hex; use function count; use function implode; use function is_int; +use function is_resource; use function is_string; +use function json_encode; use function key; +use function preg_replace; +use function sprintf; /** * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like @@ -114,6 +121,9 @@ class Connection implements DriverConnection */ private $platform; + /** @var ExceptionConverter|null */ + private $exceptionConverter; + /** * The schema manager. * @@ -284,7 +294,7 @@ public function connect() try { $this->_conn = $this->_driver->connect($this->params); } catch (DriverException $e) { - throw DBALException::driverException($this->_driver, $e); + throw $this->convertException($e); } $this->transactionNestingLevel = 0; @@ -467,8 +477,8 @@ public function fetchAssociative(string $query, array $params = [], array $types { try { return $this->executeQuery($query, $params, $types)->fetchAssociative(); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -488,8 +498,8 @@ public function fetchNumeric(string $query, array $params = [], array $types = [ { try { return $this->executeQuery($query, $params, $types)->fetchNumeric(); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -509,8 +519,8 @@ public function fetchOne(string $query, array $params = [], array $types = []) { try { return $this->executeQuery($query, $params, $types)->fetchOne(); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -771,8 +781,8 @@ public function fetchAllNumeric(string $query, array $params = [], array $types { try { return $this->executeQuery($query, $params, $types)->fetchAllNumeric(); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -791,8 +801,8 @@ public function fetchAllAssociative(string $query, array $params = [], array $ty { try { return $this->executeQuery($query, $params, $types)->fetchAllAssociative(); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -811,8 +821,8 @@ public function fetchFirstColumn(string $query, array $params = [], array $types { try { return $this->executeQuery($query, $params, $types)->fetchFirstColumn(); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -835,8 +845,8 @@ public function iterateNumeric(string $query, array $params = [], array $types = while (($row = $result->fetchNumeric()) !== false) { yield $row; } - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -859,8 +869,8 @@ public function iterateAssociative(string $query, array $params = [], array $typ while (($row = $result->fetchAssociative()) !== false) { yield $row; } - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -883,8 +893,8 @@ public function iterateColumn(string $query, array $params = [], array $types = while (($value = $result->fetchOne()) !== false) { yield $value; } - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } } @@ -899,11 +909,7 @@ public function iterateColumn(string $query, array $params = [], array $types = */ public function prepare(string $sql): DriverStatement { - try { - return new Statement($sql, $this); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $sql); - } + return new Statement($sql, $this); } /** @@ -952,8 +958,8 @@ public function executeQuery( } return new Result($result, $this); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } finally { if ($logger !== null) { $logger->stopQuery(); @@ -1010,6 +1016,9 @@ public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qc return new Result($result, $this); } + /** + * @throws DBALException + */ public function query(string $sql): DriverResult { $connection = $this->getWrappedConnection(); @@ -1021,8 +1030,8 @@ public function query(string $sql): DriverResult try { return $connection->query($sql); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $sql); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $sql); } finally { if ($logger !== null) { $logger->stopQuery(); @@ -1069,8 +1078,8 @@ public function executeUpdate(string $query, array $params = [], array $types = } return $connection->exec($query); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $query, $params, $types); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $query, $params, $types); } finally { if ($logger !== null) { $logger->stopQuery(); @@ -1078,6 +1087,9 @@ public function executeUpdate(string $query, array $params = [], array $types = } } + /** + * @throws DBALException + */ public function exec(string $statement): int { $connection = $this->getWrappedConnection(); @@ -1089,8 +1101,8 @@ public function exec(string $statement): int try { return $connection->exec($statement); - } catch (Throwable $e) { - $this->handleExceptionDuringQuery($e, $statement); + } catch (DriverException $e) { + throw $this->convertExceptionDuringQuery($e, $statement); } finally { if ($logger !== null) { $logger->stopQuery(); @@ -1578,15 +1590,12 @@ private function getBindingInfo($value, $type) /** * Resolves the parameters to a format which can be displayed. * - * @internal This is a purely internal method. If you rely on this method, you are advised to - * copy/paste the code as this method may change, or be removed without prior notice. - * * @param mixed[] $params * @param array $types * * @return mixed[] */ - public function resolveParams(array $params, array $types) + private function resolveParams(array $params, array $types): array { $resolvedParams = []; @@ -1638,53 +1647,73 @@ public function createQueryBuilder() * * @param array $params * @param array $types - * - * @throws DBALException - * - * @psalm-return never-return */ - public function handleExceptionDuringQuery(Throwable $e, string $sql, array $params = [], array $types = []): void - { - $this->throw( - DBALException::driverExceptionDuringQuery( - $this->_driver, - $e, - $sql, + final public function convertExceptionDuringQuery( + DriverException $e, + string $sql, + array $params = [], + array $types = [] + ): DBALException { + $message = "An exception occurred while executing '" . $sql . "'"; + + if (count($params) > 0) { + $message .= ' with params ' . $this->formatParameters( $this->resolveParams($params, $types) - ) - ); + ); + } + + $message .= ":\n\n" . $e->getMessage(); + + return $this->handleDriverException($e, $message); } /** * @internal - * - * @throws DBALException - * - * @psalm-return never-return */ - public function handleDriverException(Throwable $e): void + final public function convertException(DriverException $e): DBALException { - $this->throw( - DBALException::driverException( - $this->_driver, - $e - ) + return $this->handleDriverException( + $e, + 'An exception occurred in driver: ' . $e->getMessage() ); } /** - * @internal - * - * @throws DBALException + * Returns a human-readable representation of an array of parameters. + * This properly handles binary data by returning a hex representation. * - * @psalm-return never-return + * @param mixed[] $params */ - private function throw(DBALException $e): void + private function formatParameters(array $params): string + { + return '[' . implode(', ', array_map(static function ($param): string { + if (is_resource($param)) { + return (string) $param; + } + + $json = @json_encode($param); + + if (! is_string($json) || $json === 'null' && is_string($param)) { + // JSON encoding failed, this is not a UTF-8 string. + return sprintf('"%s"', preg_replace('/.{2}/', '\\x$0', bin2hex($param))); + } + + return $json; + }, $params)) . ']'; + } + + private function handleDriverException(DriverException $driverException, string $message): DBALException { - if ($e instanceof ConnectionLost) { + if ($this->exceptionConverter === null) { + $this->exceptionConverter = $this->_driver->getExceptionConverter(); + } + + $exception = $this->exceptionConverter->convert($message, $driverException); + + if ($exception instanceof ConnectionLost) { $this->close(); } - throw $e; + return $exception; } } diff --git a/src/Connections/PrimaryReadReplicaConnection.php b/src/Connections/PrimaryReadReplicaConnection.php index cd6858dd729..4ddb5d6b385 100644 --- a/src/Connections/PrimaryReadReplicaConnection.php +++ b/src/Connections/PrimaryReadReplicaConnection.php @@ -220,6 +220,8 @@ public function ensureConnectedToReplica(): bool * @param string $connectionName * * @return DriverConnection + * + * @throws DBALException */ protected function connectTo($connectionName) { @@ -230,7 +232,7 @@ protected function connectTo($connectionName) try { return $this->_driver->connect($connectionParams); } catch (DriverException $e) { - throw DBALException::driverException($this->_driver, $e); + throw $this->convertException($e); } } diff --git a/src/DBALException.php b/src/DBALException.php index 43069567c65..8a45f816f99 100644 --- a/src/DBALException.php +++ b/src/DBALException.php @@ -2,24 +2,14 @@ namespace Doctrine\DBAL; -use Doctrine\DBAL\Driver\Exception as TheDriverException; -use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; use Exception; -use Throwable; -use function array_map; -use function bin2hex; -use function count; use function get_class; use function gettype; use function implode; use function is_object; -use function is_resource; -use function is_string; -use function json_encode; -use function preg_replace; use function spl_object_hash; use function sprintf; @@ -133,74 +123,6 @@ public static function unknownDriver($unknownDriverName, array $knownDrivers) 'Doctrine currently supports only the following drivers: ' . implode(', ', $knownDrivers)); } - /** - * @param string $sql - * @param mixed[] $params - * - * @return self - */ - public static function driverExceptionDuringQuery(Driver $driver, Throwable $driverEx, $sql, array $params = []) - { - $msg = "An exception occurred while executing '" . $sql . "'"; - if (count($params) > 0) { - $msg .= ' with params ' . self::formatParameters($params); - } - - $msg .= ":\n\n" . $driverEx->getMessage(); - - return static::wrapException($driver, $driverEx, $msg); - } - - /** - * @return self - */ - public static function driverException(Driver $driver, Throwable $driverEx) - { - return static::wrapException($driver, $driverEx, 'An exception occurred in driver: ' . $driverEx->getMessage()); - } - - /** - * @return self - */ - private static function wrapException(Driver $driver, Throwable $driverEx, string $msg) - { - if ($driverEx instanceof DriverException) { - return $driverEx; - } - - if ($driverEx instanceof TheDriverException) { - return $driver->convertException($msg, $driverEx); - } - - return new self($msg, 0, $driverEx); - } - - /** - * Returns a human-readable representation of an array of parameters. - * This properly handles binary data by returning a hex representation. - * - * @param mixed[] $params - * - * @return string - */ - private static function formatParameters(array $params) - { - return '[' . implode(', ', array_map(static function ($param): string { - if (is_resource($param)) { - return (string) $param; - } - - $json = @json_encode($param); - - if (! is_string($json) || $json === 'null' && is_string($param)) { - // JSON encoding failed, this is not a UTF-8 string. - return sprintf('"%s"', preg_replace('/.{2}/', '\\x$0', bin2hex($param))); - } - - return $json; - }, $params)) . ']'; - } - /** * @param string $wrapperClass * diff --git a/src/Driver.php b/src/Driver.php index 3d7b8d386b3..2c02495a235 100644 --- a/src/Driver.php +++ b/src/Driver.php @@ -2,9 +2,9 @@ namespace Doctrine\DBAL; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Driver\Connection as DriverConnection; use Doctrine\DBAL\Driver\Exception; -use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Schema\AbstractSchemaManager; @@ -42,15 +42,7 @@ public function getDatabasePlatform(); public function getSchemaManager(Connection $conn); /** - * Converts a given driver-level exception into a DBAL-level driver exception. - * - * Implementors should use the vendor-specific error code and SQLSTATE of the exception - * and instantiate the most appropriate specialized {@link DriverException} subclass. - * - * @param string $message The exception message to use. - * @param Exception $exception The driver exception to convert. - * - * @return DriverException An instance of {@link DriverException} or one of its subclasses. + * Gets the ExceptionConverter that can be used to convert driver-level exceptions into DBAL exceptions. */ - public function convertException($message, Exception $exception); + public function getExceptionConverter(): ExceptionConverter; } diff --git a/src/Driver/API/DefaultExceptionConverter.php b/src/Driver/API/DefaultExceptionConverter.php new file mode 100644 index 00000000000..fa28b4fe473 --- /dev/null +++ b/src/Driver/API/DefaultExceptionConverter.php @@ -0,0 +1,16 @@ +getCode()) { + case 1213: + return new DeadlockException($message, $exception); + + case 1205: + return new LockWaitTimeoutException($message, $exception); + + case 1050: + return new TableExistsException($message, $exception); + + case 1051: + case 1146: + return new TableNotFoundException($message, $exception); + + case 1216: + case 1217: + case 1451: + case 1452: + case 1701: + return new ForeignKeyConstraintViolationException($message, $exception); + + case 1062: + case 1557: + case 1569: + case 1586: + return new UniqueConstraintViolationException($message, $exception); + + case 1054: + case 1166: + case 1611: + return new InvalidFieldNameException($message, $exception); + + case 1052: + case 1060: + case 1110: + return new NonUniqueFieldNameException($message, $exception); + + case 1064: + case 1149: + case 1287: + case 1341: + case 1342: + case 1343: + case 1344: + case 1382: + case 1479: + case 1541: + case 1554: + case 1626: + return new SyntaxErrorException($message, $exception); + + case 1044: + case 1045: + case 1046: + case 1049: + case 1095: + case 1142: + case 1143: + case 1227: + case 1370: + case 1429: + case 2002: + case 2005: + return new ConnectionException($message, $exception); + + case 2006: + return new ConnectionLost($message, $exception); + + case 1048: + case 1121: + case 1138: + case 1171: + case 1252: + case 1263: + case 1364: + case 1566: + return new NotNullConstraintViolationException($message, $exception); + } + + return new DriverException($message, $exception); + } +} diff --git a/src/Driver/API/OCI/ExceptionConverter.php b/src/Driver/API/OCI/ExceptionConverter.php new file mode 100644 index 00000000000..e98be404d35 --- /dev/null +++ b/src/Driver/API/OCI/ExceptionConverter.php @@ -0,0 +1,64 @@ +getCode()) { + case 1: + case 2299: + case 38911: + return new UniqueConstraintViolationException($message, $exception); + + case 904: + return new InvalidFieldNameException($message, $exception); + + case 918: + case 960: + return new NonUniqueFieldNameException($message, $exception); + + case 923: + return new SyntaxErrorException($message, $exception); + + case 942: + return new TableNotFoundException($message, $exception); + + case 955: + return new TableExistsException($message, $exception); + + case 1017: + case 12545: + return new ConnectionException($message, $exception); + + case 1400: + return new NotNullConstraintViolationException($message, $exception); + + case 2266: + case 2291: + case 2292: + return new ForeignKeyConstraintViolationException($message, $exception); + } + + return new DriverException($message, $exception); + } +} diff --git a/src/Driver/API/PostgreSQL/ExceptionConverter.php b/src/Driver/API/PostgreSQL/ExceptionConverter.php new file mode 100644 index 00000000000..0143e776216 --- /dev/null +++ b/src/Driver/API/PostgreSQL/ExceptionConverter.php @@ -0,0 +1,78 @@ +getSQLState()) { + case '40001': + case '40P01': + return new DeadlockException($message, $exception); + + case '0A000': + // Foreign key constraint violations during a TRUNCATE operation + // are considered "feature not supported" in PostgreSQL. + if (strpos($exception->getMessage(), 'truncate') !== false) { + return new ForeignKeyConstraintViolationException($message, $exception); + } + + break; + + case '23502': + return new NotNullConstraintViolationException($message, $exception); + + case '23503': + return new ForeignKeyConstraintViolationException($message, $exception); + + case '23505': + return new UniqueConstraintViolationException($message, $exception); + + case '42601': + return new SyntaxErrorException($message, $exception); + + case '42702': + return new NonUniqueFieldNameException($message, $exception); + + case '42703': + return new InvalidFieldNameException($message, $exception); + + case '42P01': + return new TableNotFoundException($message, $exception); + + case '42P07': + return new TableExistsException($message, $exception); + } + + // In some case (mainly connection errors) the PDO exception does not provide a SQLSTATE via its code. + // The exception code is always set to 7 here. + // We have to match against the SQLSTATE in the error message in these cases. + if ($exception->getCode() === 7 && strpos($exception->getMessage(), 'SQLSTATE[08006]') !== false) { + return new ConnectionException($message, $exception); + } + + return new DriverException($message, $exception); + } +} diff --git a/src/Driver/API/SQLite/ExceptionConverter.php b/src/Driver/API/SQLite/ExceptionConverter.php new file mode 100644 index 00000000000..9e6529b9d13 --- /dev/null +++ b/src/Driver/API/SQLite/ExceptionConverter.php @@ -0,0 +1,85 @@ +getMessage(), 'database is locked') !== false) { + return new LockWaitTimeoutException($message, $exception); + } + + if ( + strpos($exception->getMessage(), 'must be unique') !== false || + strpos($exception->getMessage(), 'is not unique') !== false || + strpos($exception->getMessage(), 'are not unique') !== false || + strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false + ) { + return new UniqueConstraintViolationException($message, $exception); + } + + if ( + strpos($exception->getMessage(), 'may not be NULL') !== false || + strpos($exception->getMessage(), 'NOT NULL constraint failed') !== false + ) { + return new NotNullConstraintViolationException($message, $exception); + } + + if (strpos($exception->getMessage(), 'no such table:') !== false) { + return new TableNotFoundException($message, $exception); + } + + if (strpos($exception->getMessage(), 'already exists') !== false) { + return new TableExistsException($message, $exception); + } + + if (strpos($exception->getMessage(), 'has no column named') !== false) { + return new InvalidFieldNameException($message, $exception); + } + + if (strpos($exception->getMessage(), 'ambiguous column name') !== false) { + return new NonUniqueFieldNameException($message, $exception); + } + + if (strpos($exception->getMessage(), 'syntax error') !== false) { + return new SyntaxErrorException($message, $exception); + } + + if (strpos($exception->getMessage(), 'attempt to write a readonly database') !== false) { + return new ReadOnlyException($message, $exception); + } + + if (strpos($exception->getMessage(), 'unable to open database file') !== false) { + return new ConnectionException($message, $exception); + } + + if (strpos($exception->getMessage(), 'FOREIGN KEY constraint failed') !== false) { + return new ForeignKeyConstraintViolationException($message, $exception); + } + + return new DriverException($message, $exception); + } +} diff --git a/src/Driver/AbstractDB2Driver.php b/src/Driver/AbstractDB2Driver.php index 997b78c407d..8314a8fd0d6 100644 --- a/src/Driver/AbstractDB2Driver.php +++ b/src/Driver/AbstractDB2Driver.php @@ -4,7 +4,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Exception\DriverException; +use Doctrine\DBAL\Driver\API\DefaultExceptionConverter; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Platforms\DB2Platform; use Doctrine\DBAL\Schema\DB2SchemaManager; @@ -29,13 +30,8 @@ public function getSchemaManager(Connection $conn) return new DB2SchemaManager($conn); } - /** - * @param string $message - * - * @return DriverException - */ - public function convertException($message, Exception $exception) + public function getExceptionConverter(): ExceptionConverter { - return new DriverException($message, $exception); + return new DefaultExceptionConverter(); } } diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index f1e02359bb0..fced7a3f8b5 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -4,19 +4,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Exception\ConnectionException; -use Doctrine\DBAL\Exception\ConnectionLost; -use Doctrine\DBAL\Exception\DeadlockException; -use Doctrine\DBAL\Exception\DriverException; -use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; -use Doctrine\DBAL\Exception\InvalidFieldNameException; -use Doctrine\DBAL\Exception\LockWaitTimeoutException; -use Doctrine\DBAL\Exception\NonUniqueFieldNameException; -use Doctrine\DBAL\Exception\NotNullConstraintViolationException; -use Doctrine\DBAL\Exception\SyntaxErrorException; -use Doctrine\DBAL\Exception\TableExistsException; -use Doctrine\DBAL\Exception\TableNotFoundException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\MySQL; use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; @@ -33,96 +22,6 @@ */ abstract class AbstractMySQLDriver implements VersionAwarePlatformDriver { - /** - * {@inheritdoc} - * - * @link https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html - * @link https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html - */ - public function convertException($message, Exception $exception) - { - switch ($exception->getCode()) { - case 1213: - return new DeadlockException($message, $exception); - - case 1205: - return new LockWaitTimeoutException($message, $exception); - - case 1050: - return new TableExistsException($message, $exception); - - case 1051: - case 1146: - return new TableNotFoundException($message, $exception); - - case 1216: - case 1217: - case 1451: - case 1452: - case 1701: - return new ForeignKeyConstraintViolationException($message, $exception); - - case 1062: - case 1557: - case 1569: - case 1586: - return new UniqueConstraintViolationException($message, $exception); - - case 1054: - case 1166: - case 1611: - return new InvalidFieldNameException($message, $exception); - - case 1052: - case 1060: - case 1110: - return new NonUniqueFieldNameException($message, $exception); - - case 1064: - case 1149: - case 1287: - case 1341: - case 1342: - case 1343: - case 1344: - case 1382: - case 1479: - case 1541: - case 1554: - case 1626: - return new SyntaxErrorException($message, $exception); - - case 1044: - case 1045: - case 1046: - case 1049: - case 1095: - case 1142: - case 1143: - case 1227: - case 1370: - case 1429: - case 2002: - case 2005: - return new ConnectionException($message, $exception); - - case 2006: - return new ConnectionLost($message, $exception); - - case 1048: - case 1121: - case 1138: - case 1171: - case 1252: - case 1263: - case 1364: - case 1566: - return new NotNullConstraintViolationException($message, $exception); - } - - return new DriverException($message, $exception); - } - /** * {@inheritdoc} * @@ -228,4 +127,9 @@ public function getSchemaManager(Connection $conn) { return new MySqlSchemaManager($conn); } + + public function getExceptionConverter(): ExceptionConverter + { + return new MySQL\ExceptionConverter(); + } } diff --git a/src/Driver/AbstractOracleDriver.php b/src/Driver/AbstractOracleDriver.php index d4458e3a22a..5ab16b89da0 100644 --- a/src/Driver/AbstractOracleDriver.php +++ b/src/Driver/AbstractOracleDriver.php @@ -5,16 +5,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\AbstractOracleDriver\EasyConnectString; -use Doctrine\DBAL\Exception\ConnectionException; -use Doctrine\DBAL\Exception\DriverException; -use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; -use Doctrine\DBAL\Exception\InvalidFieldNameException; -use Doctrine\DBAL\Exception\NonUniqueFieldNameException; -use Doctrine\DBAL\Exception\NotNullConstraintViolationException; -use Doctrine\DBAL\Exception\SyntaxErrorException; -use Doctrine\DBAL\Exception\TableExistsException; -use Doctrine\DBAL\Exception\TableNotFoundException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\OCI; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Schema\OracleSchemaManager; @@ -23,49 +15,6 @@ */ abstract class AbstractOracleDriver implements Driver { - /** - * {@inheritdoc} - */ - public function convertException($message, Exception $exception) - { - switch ($exception->getCode()) { - case 1: - case 2299: - case 38911: - return new UniqueConstraintViolationException($message, $exception); - - case 904: - return new InvalidFieldNameException($message, $exception); - - case 918: - case 960: - return new NonUniqueFieldNameException($message, $exception); - - case 923: - return new SyntaxErrorException($message, $exception); - - case 942: - return new TableNotFoundException($message, $exception); - - case 955: - return new TableExistsException($message, $exception); - - case 1017: - case 12545: - return new ConnectionException($message, $exception); - - case 1400: - return new NotNullConstraintViolationException($message, $exception); - - case 2266: - case 2291: - case 2292: - return new ForeignKeyConstraintViolationException($message, $exception); - } - - return new DriverException($message, $exception); - } - /** * {@inheritdoc} */ @@ -82,6 +31,11 @@ public function getSchemaManager(Connection $conn) return new OracleSchemaManager($conn); } + public function getExceptionConverter(): ExceptionConverter + { + return new OCI\ExceptionConverter(); + } + /** * Returns an appropriate Easy Connect String for the given parameters. * diff --git a/src/Driver/AbstractPostgreSQLDriver.php b/src/Driver/AbstractPostgreSQLDriver.php index a69cebba66f..1e22ce6074c 100644 --- a/src/Driver/AbstractPostgreSQLDriver.php +++ b/src/Driver/AbstractPostgreSQLDriver.php @@ -4,24 +4,14 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Exception\ConnectionException; -use Doctrine\DBAL\Exception\DeadlockException; -use Doctrine\DBAL\Exception\DriverException; -use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; -use Doctrine\DBAL\Exception\InvalidFieldNameException; -use Doctrine\DBAL\Exception\NonUniqueFieldNameException; -use Doctrine\DBAL\Exception\NotNullConstraintViolationException; -use Doctrine\DBAL\Exception\SyntaxErrorException; -use Doctrine\DBAL\Exception\TableExistsException; -use Doctrine\DBAL\Exception\TableNotFoundException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\PostgreSQL; use Doctrine\DBAL\Platforms\PostgreSQL100Platform; use Doctrine\DBAL\Platforms\PostgreSQL94Platform; use Doctrine\DBAL\Schema\PostgreSqlSchemaManager; use Doctrine\DBAL\VersionAwarePlatformDriver; use function preg_match; -use function strpos; use function version_compare; /** @@ -29,62 +19,6 @@ */ abstract class AbstractPostgreSQLDriver implements VersionAwarePlatformDriver { - /** - * {@inheritdoc} - * - * @link http://www.postgresql.org/docs/9.4/static/errcodes-appendix.html - */ - public function convertException($message, Exception $exception) - { - switch ($exception->getSQLState()) { - case '40001': - case '40P01': - return new DeadlockException($message, $exception); - - case '0A000': - // Foreign key constraint violations during a TRUNCATE operation - // are considered "feature not supported" in PostgreSQL. - if (strpos($exception->getMessage(), 'truncate') !== false) { - return new ForeignKeyConstraintViolationException($message, $exception); - } - - break; - - case '23502': - return new NotNullConstraintViolationException($message, $exception); - - case '23503': - return new ForeignKeyConstraintViolationException($message, $exception); - - case '23505': - return new UniqueConstraintViolationException($message, $exception); - - case '42601': - return new SyntaxErrorException($message, $exception); - - case '42702': - return new NonUniqueFieldNameException($message, $exception); - - case '42703': - return new InvalidFieldNameException($message, $exception); - - case '42P01': - return new TableNotFoundException($message, $exception); - - case '42P07': - return new TableExistsException($message, $exception); - } - - // In some case (mainly connection errors) the PDO exception does not provide a SQLSTATE via its code. - // The exception code is always set to 7 here. - // We have to match against the SQLSTATE in the error message in these cases. - if ($exception->getCode() === 7 && strpos($exception->getMessage(), 'SQLSTATE[08006]') !== false) { - return new ConnectionException($message, $exception); - } - - return new DriverException($message, $exception); - } - /** * {@inheritdoc} */ @@ -124,4 +58,9 @@ public function getSchemaManager(Connection $conn) { return new PostgreSqlSchemaManager($conn); } + + public function getExceptionConverter(): ExceptionConverter + { + return new PostgreSQL\ExceptionConverter(); + } } diff --git a/src/Driver/AbstractSQLServerDriver.php b/src/Driver/AbstractSQLServerDriver.php index 47661e2d5d6..829428342ce 100644 --- a/src/Driver/AbstractSQLServerDriver.php +++ b/src/Driver/AbstractSQLServerDriver.php @@ -4,7 +4,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Exception\DriverException; +use Doctrine\DBAL\Driver\API\DefaultExceptionConverter; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Platforms\SQLServer2012Platform; use Doctrine\DBAL\Schema\SQLServerSchemaManager; @@ -29,13 +30,8 @@ public function getSchemaManager(Connection $conn) return new SQLServerSchemaManager($conn); } - /** - * @param string $message - * - * @return DriverException - */ - public function convertException($message, Exception $exception) + public function getExceptionConverter(): ExceptionConverter { - return new DriverException($message, $exception); + return new DefaultExceptionConverter(); } } diff --git a/src/Driver/AbstractSQLiteDriver.php b/src/Driver/AbstractSQLiteDriver.php index e61c308e210..9b32b0c51bd 100644 --- a/src/Driver/AbstractSQLiteDriver.php +++ b/src/Driver/AbstractSQLiteDriver.php @@ -4,90 +4,16 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Exception\ConnectionException; -use Doctrine\DBAL\Exception\DriverException; -use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; -use Doctrine\DBAL\Exception\InvalidFieldNameException; -use Doctrine\DBAL\Exception\LockWaitTimeoutException; -use Doctrine\DBAL\Exception\NonUniqueFieldNameException; -use Doctrine\DBAL\Exception\NotNullConstraintViolationException; -use Doctrine\DBAL\Exception\ReadOnlyException; -use Doctrine\DBAL\Exception\SyntaxErrorException; -use Doctrine\DBAL\Exception\TableExistsException; -use Doctrine\DBAL\Exception\TableNotFoundException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\SQLite; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Schema\SqliteSchemaManager; -use function strpos; - /** * Abstract base implementation of the {@link Doctrine\DBAL\Driver} interface for SQLite based drivers. */ abstract class AbstractSQLiteDriver implements Driver { - /** - * {@inheritdoc} - * - * @link http://www.sqlite.org/c3ref/c_abort.html - */ - public function convertException($message, Exception $exception) - { - if (strpos($exception->getMessage(), 'database is locked') !== false) { - return new LockWaitTimeoutException($message, $exception); - } - - if ( - strpos($exception->getMessage(), 'must be unique') !== false || - strpos($exception->getMessage(), 'is not unique') !== false || - strpos($exception->getMessage(), 'are not unique') !== false || - strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false - ) { - return new UniqueConstraintViolationException($message, $exception); - } - - if ( - strpos($exception->getMessage(), 'may not be NULL') !== false || - strpos($exception->getMessage(), 'NOT NULL constraint failed') !== false - ) { - return new NotNullConstraintViolationException($message, $exception); - } - - if (strpos($exception->getMessage(), 'no such table:') !== false) { - return new TableNotFoundException($message, $exception); - } - - if (strpos($exception->getMessage(), 'already exists') !== false) { - return new TableExistsException($message, $exception); - } - - if (strpos($exception->getMessage(), 'has no column named') !== false) { - return new InvalidFieldNameException($message, $exception); - } - - if (strpos($exception->getMessage(), 'ambiguous column name') !== false) { - return new NonUniqueFieldNameException($message, $exception); - } - - if (strpos($exception->getMessage(), 'syntax error') !== false) { - return new SyntaxErrorException($message, $exception); - } - - if (strpos($exception->getMessage(), 'attempt to write a readonly database') !== false) { - return new ReadOnlyException($message, $exception); - } - - if (strpos($exception->getMessage(), 'unable to open database file') !== false) { - return new ConnectionException($message, $exception); - } - - if (strpos($exception->getMessage(), 'FOREIGN KEY constraint failed') !== false) { - return new ForeignKeyConstraintViolationException($message, $exception); - } - - return new DriverException($message, $exception); - } - /** * {@inheritdoc} */ @@ -103,4 +29,9 @@ public function getSchemaManager(Connection $conn) { return new SqliteSchemaManager($conn); } + + public function getExceptionConverter(): ExceptionConverter + { + return new SQLite\ExceptionConverter(); + } } diff --git a/src/Result.php b/src/Result.php index d9520648e3f..8a8410fa9fc 100644 --- a/src/Result.php +++ b/src/Result.php @@ -33,7 +33,7 @@ public function fetchNumeric() try { return $this->result->fetchNumeric(); } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -47,19 +47,21 @@ public function fetchAssociative() try { return $this->result->fetchAssociative(); } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } /** * {@inheritDoc} + * + * @throws DBALException */ public function fetchOne() { try { return $this->result->fetchOne(); } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -73,7 +75,7 @@ public function fetchAllNumeric(): array try { return $this->result->fetchAllNumeric(); } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -87,7 +89,7 @@ public function fetchAllAssociative(): array try { return $this->result->fetchAllAssociative(); } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -101,7 +103,7 @@ public function fetchFirstColumn(): array try { return $this->result->fetchFirstColumn(); } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -117,7 +119,7 @@ public function iterateNumeric(): Traversable yield $row; } } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -133,7 +135,7 @@ public function iterateAssociative(): Traversable yield $row; } } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } @@ -149,7 +151,7 @@ public function iterateColumn(): Traversable yield $value; } } catch (DriverException $e) { - $this->connection->handleDriverException($e); + throw $this->connection->convertException($e); } } diff --git a/src/Statement.php b/src/Statement.php index 1a74ce05503..b1404195d6b 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -2,11 +2,11 @@ namespace Doctrine\DBAL; +use Doctrine\DBAL\Driver\Exception; use Doctrine\DBAL\Driver\Result as DriverResult; use Doctrine\DBAL\Driver\Statement as DriverStatement; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; -use Throwable; use function is_string; @@ -65,11 +65,21 @@ class Statement implements DriverStatement * * @param string $sql The SQL of the statement. * @param Connection $conn The connection on which the statement should be executed. + * + * @throws DBALException */ public function __construct($sql, Connection $conn) { + $driverConnection = $conn->getWrappedConnection(); + + try { + $stmt = $driverConnection->prepare($sql); + } catch (Exception $ex) { + throw $conn->convertExceptionDuringQuery($ex, $sql); + } + $this->sql = $sql; - $this->stmt = $conn->getWrappedConnection()->prepare($sql); + $this->stmt = $stmt; $this->conn = $conn; $this->platform = $conn->getDatabasePlatform(); } @@ -154,8 +164,8 @@ public function execute($params = null): DriverResult $this->stmt->execute($params), $this->conn ); - } catch (Throwable $ex) { - $this->conn->handleExceptionDuringQuery($ex, $this->sql, $this->params, $this->types); + } catch (Exception $ex) { + throw $this->conn->convertExceptionDuringQuery($ex, $this->sql, $this->params, $this->types); } finally { if ($logger !== null) { $logger->stopQuery(); diff --git a/tests/Connection/ExceptionHandlingTest.php b/tests/Connection/ExceptionHandlingTest.php new file mode 100644 index 00000000000..9412eb42dc1 --- /dev/null +++ b/tests/Connection/ExceptionHandlingTest.php @@ -0,0 +1,49 @@ +connection = new Connection([], $this->createConfiguredMock(Driver::class, [ + 'getExceptionConverter' => new DefaultExceptionConverter(), + ])); + } + + public function testDriverExceptionDuringQueryAcceptsBinaryData(): void + { + $e = $this->connection->convertExceptionDuringQuery( + $this->createMock(DriverException::class), + '', + ['ABC', chr(128)] + ); + + self::assertStringContainsString('with params ["ABC", "\x80"]', $e->getMessage()); + } + + public function testDriverExceptionDuringQueryAcceptsResource(): void + { + $e = $this->connection->convertExceptionDuringQuery( + $this->createMock(DriverException::class), + 'INSERT INTO file (`content`) VALUES (?)', + [ + 1 => fopen(__FILE__, 'r'), + ] + ); + + self::assertStringContainsString('Resource', $e->getMessage()); + } +} diff --git a/tests/DBALExceptionTest.php b/tests/DBALExceptionTest.php index 232692bb588..c2394718482 100644 --- a/tests/DBALExceptionTest.php +++ b/tests/DBALExceptionTest.php @@ -3,44 +3,13 @@ namespace Doctrine\DBAL\Tests; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Driver\Exception as InnerDriverException; -use Doctrine\DBAL\Exception\DriverException; -use Exception; use PHPUnit\Framework\TestCase; use stdClass; -use function chr; -use function fopen; use function sprintf; class DBALExceptionTest extends TestCase { - public function testDriverExceptionDuringQueryAcceptsBinaryData(): void - { - $driver = $this->createMock(Driver::class); - $e = DBALException::driverExceptionDuringQuery($driver, new Exception(), '', ['ABC', chr(128)]); - self::assertStringContainsString('with params ["ABC", "\x80"]', $e->getMessage()); - } - - public function testDriverExceptionDuringQueryAcceptsResource(): void - { - $driver = $this->createMock(Driver::class); - $e = DBALException::driverExceptionDuringQuery($driver, new Exception(), 'INSERT INTO file (`content`) VALUES (?)', [1 => fopen(__FILE__, 'r')]); - self::assertStringContainsString('Resource', $e->getMessage()); - } - - public function testAvoidOverWrappingOnDriverException(): void - { - $driver = $this->createMock(Driver::class); - - $inner = $this->createMock(InnerDriverException::class); - - $ex = new DriverException('', $inner); - $e = DBALException::driverExceptionDuringQuery($driver, $ex, ''); - self::assertSame($ex, $e); - } - public function testDriverRequiredWithUrl(): void { $url = 'mysql://localhost'; diff --git a/tests/Driver/API/ExceptionConverterTest.php b/tests/Driver/API/ExceptionConverterTest.php new file mode 100644 index 00000000000..c737cfb8382 --- /dev/null +++ b/tests/Driver/API/ExceptionConverterTest.php @@ -0,0 +1,71 @@ +converter = $this->createConverter(); + } + + abstract protected function createConverter(): ExceptionConverter; + + /** + * @dataProvider exceptionConversionProvider + */ + public function testConvertsException( + string $expectedClass, + int $errorCode, + ?string $sqlState = null, + string $message = '' + ): void { + $driverException = $this->getMockForAbstractClass( + AbstractException::class, + [$message, $sqlState, $errorCode] + ); + + $dbalMessage = 'DBAL exception message'; + $dbalException = $this->converter->convert($dbalMessage, $driverException); + + self::assertInstanceOf($expectedClass, $dbalException); + + self::assertSame($driverException->getCode(), $dbalException->getCode()); + self::assertSame($driverException->getSQLState(), $dbalException->getSQLState()); + self::assertSame($driverException, $dbalException->getPrevious()); + self::assertSame($dbalMessage, $dbalException->getMessage()); + } + + /** + * @return iterable + */ + public static function exceptionConversionProvider(): iterable + { + foreach (static::getExceptionConversionData() as $expectedClass => $items) { + foreach ($items as $item) { + yield array_merge([$expectedClass], $item); + } + } + + yield [DriverException::class, 1, 'HY000', 'The message']; + } + + /** + * @return array + */ + abstract protected static function getExceptionConversionData(): array; +} diff --git a/tests/Driver/API/MySQL/ExceptionConverterTest.php b/tests/Driver/API/MySQL/ExceptionConverterTest.php new file mode 100644 index 00000000000..79a86875685 --- /dev/null +++ b/tests/Driver/API/MySQL/ExceptionConverterTest.php @@ -0,0 +1,109 @@ + [ + [1044], + [1045], + [1046], + [1049], + [1095], + [1142], + [1143], + [1227], + [1370], + [2002], + [2005], + ], + ForeignKeyConstraintViolationException::class => [ + [1216], + [1217], + [1451], + [1452], + ], + InvalidFieldNameException::class => [ + [1054], + [1166], + [1611], + ], + NonUniqueFieldNameException::class => [ + [1052], + [1060], + [1110], + ], + NotNullConstraintViolationException::class => [ + [1048], + [1121], + [1138], + [1171], + [1252], + [1263], + [1364], + [1566], + ], + SyntaxErrorException::class => [ + [1064], + [1149], + [1287], + [1341], + [1342], + [1343], + [1344], + [1382], + [1479], + [1541], + [1554], + [1626], + ], + TableExistsException::class => [ + [1050], + ], + TableNotFoundException::class => [ + [1051], + [1146], + ], + UniqueConstraintViolationException::class => [ + [1062], + [1557], + [1569], + [1586], + ], + DeadlockException::class => [ + [1213], + ], + LockWaitTimeoutException::class => [ + [1205], + ], + ]; + } +} diff --git a/tests/Driver/API/OCI/ExceptionConverterTest.php b/tests/Driver/API/OCI/ExceptionConverterTest.php new file mode 100644 index 00000000000..13f01331dca --- /dev/null +++ b/tests/Driver/API/OCI/ExceptionConverterTest.php @@ -0,0 +1,66 @@ + [ + [1017], + [12545], + ], + ForeignKeyConstraintViolationException::class => [ + [2292], + ], + InvalidFieldNameException::class => [ + [904], + ], + NonUniqueFieldNameException::class => [ + [918], + [960], + ], + NotNullConstraintViolationException::class => [ + [1400], + ], + SyntaxErrorException::class => [ + [923], + ], + TableExistsException::class => [ + [955], + ], + TableNotFoundException::class => [ + [942], + ], + UniqueConstraintViolationException::class => [ + [1], + [2299], + [38911], + ], + ]; + } +} diff --git a/tests/Driver/API/PostgreSQL/ExceptionConverterTest.php b/tests/Driver/API/PostgreSQL/ExceptionConverterTest.php new file mode 100644 index 00000000000..65e39096300 --- /dev/null +++ b/tests/Driver/API/PostgreSQL/ExceptionConverterTest.php @@ -0,0 +1,67 @@ + [ + [7, null, 'SQLSTATE[08006]'], + ], + ForeignKeyConstraintViolationException::class => [ + [0, '23503'], + ], + InvalidFieldNameException::class => [ + [0, '42703'], + ], + NonUniqueFieldNameException::class => [ + [0, '42702'], + ], + NotNullConstraintViolationException::class => [ + [0, '23502'], + ], + SyntaxErrorException::class => [ + [0, '42601'], + ], + TableExistsException::class => [ + [0, '42P07'], + ], + TableNotFoundException::class => [ + [0, '42P01'], + ], + UniqueConstraintViolationException::class => [ + [0, '23505'], + ], + DeadlockException::class => [ + [0, '40001'], + [0, '40P01'], + ], + ]; + } +} diff --git a/tests/Driver/API/SQLite/ExceptionConverterTest.php b/tests/Driver/API/SQLite/ExceptionConverterTest.php new file mode 100644 index 00000000000..35b4c0a575d --- /dev/null +++ b/tests/Driver/API/SQLite/ExceptionConverterTest.php @@ -0,0 +1,68 @@ + [ + [0, null, 'unable to open database file'], + ], + InvalidFieldNameException::class => [ + [0, null, 'has no column named'], + ], + NonUniqueFieldNameException::class => [ + [0, null, 'ambiguous column name'], + ], + NotNullConstraintViolationException::class => [ + [0, null, 'may not be NULL'], + ], + ReadOnlyException::class => [ + [0, null, 'attempt to write a readonly database'], + ], + SyntaxErrorException::class => [ + [0, null, 'syntax error'], + ], + TableExistsException::class => [ + [0, null, 'already exists'], + ], + TableNotFoundException::class => [ + [0, null, 'no such table:'], + ], + UniqueConstraintViolationException::class => [ + [0, null, 'must be unique'], + [0, null, 'is not unique'], + [0, null, 'are not unique'], + ], + LockWaitTimeoutException::class => [ + [0, null, 'database is locked'], + ], + ]; + } +} diff --git a/tests/Driver/AbstractDB2DriverTest.php b/tests/Driver/AbstractDB2DriverTest.php index 6bfae2a94b3..52b614c38ac 100644 --- a/tests/Driver/AbstractDB2DriverTest.php +++ b/tests/Driver/AbstractDB2DriverTest.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\AbstractDB2Driver; +use Doctrine\DBAL\Driver\API\DefaultExceptionConverter; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\DB2Platform; use Doctrine\DBAL\Schema\AbstractSchemaManager; @@ -26,4 +28,9 @@ protected function createSchemaManager(Connection $connection): AbstractSchemaMa { return new DB2SchemaManager($connection); } + + protected function createExceptionConverter(): ExceptionConverter + { + return new DefaultExceptionConverter(); + } } diff --git a/tests/Driver/AbstractDriverTest.php b/tests/Driver/AbstractDriverTest.php index 5058fc335ff..05a3468b1e8 100644 --- a/tests/Driver/AbstractDriverTest.php +++ b/tests/Driver/AbstractDriverTest.php @@ -5,26 +5,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Driver\AbstractException; -use Doctrine\DBAL\Driver\AbstractSQLServerDriver; -use Doctrine\DBAL\Driver\IBMDB2; -use Doctrine\DBAL\Exception\ConnectionException; -use Doctrine\DBAL\Exception\ConstraintViolationException; -use Doctrine\DBAL\Exception\DatabaseObjectExistsException; -use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException; -use Doctrine\DBAL\Exception\DeadlockException; -use Doctrine\DBAL\Exception\DriverException; -use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; -use Doctrine\DBAL\Exception\InvalidFieldNameException; -use Doctrine\DBAL\Exception\LockWaitTimeoutException; -use Doctrine\DBAL\Exception\NonUniqueFieldNameException; -use Doctrine\DBAL\Exception\NotNullConstraintViolationException; -use Doctrine\DBAL\Exception\ReadOnlyException; -use Doctrine\DBAL\Exception\ServerException; -use Doctrine\DBAL\Exception\SyntaxErrorException; -use Doctrine\DBAL\Exception\TableExistsException; -use Doctrine\DBAL\Exception\TableNotFoundException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\VersionAwarePlatformDriver; @@ -32,30 +13,11 @@ use PHPUnit\Framework\TestCase; use ReflectionProperty; -use function array_merge; use function get_class; use function sprintf; abstract class AbstractDriverTest extends TestCase { - public const EXCEPTION_CONNECTION = ConnectionException::class; - public const EXCEPTION_CONSTRAINT_VIOLATION = ConstraintViolationException::class; - public const EXCEPTION_DATABASE_OBJECT_EXISTS = DatabaseObjectExistsException::class; - public const EXCEPTION_DATABASE_OBJECT_NOT_FOUND = DatabaseObjectNotFoundException::class; - public const EXCEPTION_DRIVER = DriverException::class; - public const EXCEPTION_FOREIGN_KEY_CONSTRAINT_VIOLATION = ForeignKeyConstraintViolationException::class; - public const EXCEPTION_INVALID_FIELD_NAME = InvalidFieldNameException::class; - public const EXCEPTION_NON_UNIQUE_FIELD_NAME = NonUniqueFieldNameException::class; - public const EXCEPTION_NOT_NULL_CONSTRAINT_VIOLATION = NotNullConstraintViolationException::class; - public const EXCEPTION_READ_ONLY = ReadOnlyException::class; - public const EXCEPTION_SERVER = ServerException::class; - public const EXCEPTION_SYNTAX_ERROR = SyntaxErrorException::class; - public const EXCEPTION_TABLE_EXISTS = TableExistsException::class; - public const EXCEPTION_TABLE_NOT_FOUND = TableNotFoundException::class; - public const EXCEPTION_UNIQUE_CONSTRAINT_VIOLATION = UniqueConstraintViolationException::class; - public const EXCEPTION_DEADLOCK = DeadlockException::class; - public const EXCEPTION_LOCK_WAIT_TIMEOUT = LockWaitTimeoutException::class; - /** * The driver mock under test. * @@ -70,39 +32,6 @@ protected function setUp(): void $this->driver = $this->createDriver(); } - /** - * @dataProvider exceptionConversionProvider - */ - public function testConvertsException( - string $expectedClass, - int $errorCode, - ?string $sqlState = null, - string $message = '' - ): void { - if ($this->driver instanceof IBMDB2\Driver) { - self::markTestSkipped("The IBM DB2 driver currently doesn't instantiate specialized exceptions"); - } - - if ($this->driver instanceof AbstractSQLServerDriver) { - self::markTestSkipped("The SQL Server drivers currently don't instantiate specialized exceptions"); - } - - $driverException = $this->getMockForAbstractClass( - AbstractException::class, - [$message, $sqlState, $errorCode] - ); - - $dbalMessage = 'DBAL exception message'; - $dbalException = $this->driver->convertException($dbalMessage, $driverException); - - self::assertInstanceOf($expectedClass, $dbalException); - - self::assertSame($driverException->getCode(), $dbalException->getCode()); - self::assertSame($driverException->getSQLState(), $dbalException->getSQLState()); - self::assertSame($driverException, $dbalException->getPrevious()); - self::assertSame($dbalMessage, $dbalException->getMessage()); - } - public function testCreatesDatabasePlatformForVersion(): void { if (! $this->driver instanceof VersionAwarePlatformDriver) { @@ -164,6 +93,11 @@ public function testReturnsSchemaManager(): void self::assertSame($connection, $re->getValue($schemaManager)); } + public function testReturnsExceptionConverter(): void + { + self::assertEquals($this->createExceptionConverter(), $this->driver->getExceptionConverter()); + } + /** * Factory method for creating the driver instance under test. */ @@ -187,6 +121,8 @@ abstract protected function createPlatform(): AbstractPlatform; */ abstract protected function createSchemaManager(Connection $connection): AbstractSchemaManager; + abstract protected function createExceptionConverter(): ExceptionConverter; + /** * @return Connection&MockObject */ @@ -202,26 +138,4 @@ protected function getDatabasePlatformsForVersions(): array { return []; } - - /** - * @return iterable - */ - public static function exceptionConversionProvider(): iterable - { - foreach (static::getExceptionConversionData() as $expectedClass => $items) { - foreach ($items as $item) { - yield array_merge([$expectedClass], $item); - } - } - - yield [self::EXCEPTION_DRIVER, 1, 'HY000', 'The message']; - } - - /** - * @return array - */ - protected static function getExceptionConversionData(): array - { - return []; - } } diff --git a/tests/Driver/AbstractMySQLDriverTest.php b/tests/Driver/AbstractMySQLDriverTest.php index b82721a32e9..fc2bcaf2c8d 100644 --- a/tests/Driver/AbstractMySQLDriverTest.php +++ b/tests/Driver/AbstractMySQLDriverTest.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\AbstractMySQLDriver; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\MySQL; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MySQL57Platform; @@ -30,6 +32,11 @@ protected function createSchemaManager(Connection $connection): AbstractSchemaMa return new MySqlSchemaManager($connection); } + protected function createExceptionConverter(): ExceptionConverter + { + return new MySQL\ExceptionConverter(); + } + /** * {@inheritDoc} */ @@ -55,85 +62,4 @@ protected function getDatabasePlatformsForVersions(): array ['10.2.8-MariaDB-1~lenny-log', MariaDb1027Platform::class], ]; } - - /** - * {@inheritDoc} - */ - protected static function getExceptionConversionData(): array - { - return [ - self::EXCEPTION_CONNECTION => [ - [1044], - [1045], - [1046], - [1049], - [1095], - [1142], - [1143], - [1227], - [1370], - [2002], - [2005], - ], - self::EXCEPTION_FOREIGN_KEY_CONSTRAINT_VIOLATION => [ - [1216], - [1217], - [1451], - [1452], - ], - self::EXCEPTION_INVALID_FIELD_NAME => [ - [1054], - [1166], - [1611], - ], - self::EXCEPTION_NON_UNIQUE_FIELD_NAME => [ - [1052], - [1060], - [1110], - ], - self::EXCEPTION_NOT_NULL_CONSTRAINT_VIOLATION => [ - [1048], - [1121], - [1138], - [1171], - [1252], - [1263], - [1364], - [1566], - ], - self::EXCEPTION_SYNTAX_ERROR => [ - [1064], - [1149], - [1287], - [1341], - [1342], - [1343], - [1344], - [1382], - [1479], - [1541], - [1554], - [1626], - ], - self::EXCEPTION_TABLE_EXISTS => [ - [1050], - ], - self::EXCEPTION_TABLE_NOT_FOUND => [ - [1051], - [1146], - ], - self::EXCEPTION_UNIQUE_CONSTRAINT_VIOLATION => [ - [1062], - [1557], - [1569], - [1586], - ], - self::EXCEPTION_DEADLOCK => [ - [1213], - ], - self::EXCEPTION_LOCK_WAIT_TIMEOUT => [ - [1205], - ], - ]; - } } diff --git a/tests/Driver/AbstractOracleDriverTest.php b/tests/Driver/AbstractOracleDriverTest.php index a6a3130f02e..7ed26ebc43c 100644 --- a/tests/Driver/AbstractOracleDriverTest.php +++ b/tests/Driver/AbstractOracleDriverTest.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\AbstractOracleDriver; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\OCI; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Schema\AbstractSchemaManager; @@ -27,43 +29,8 @@ protected function createSchemaManager(Connection $connection): AbstractSchemaMa return new OracleSchemaManager($connection); } - /** - * {@inheritDoc} - */ - protected static function getExceptionConversionData(): array + protected function createExceptionConverter(): ExceptionConverter { - return [ - self::EXCEPTION_CONNECTION => [ - [1017], - [12545], - ], - self::EXCEPTION_FOREIGN_KEY_CONSTRAINT_VIOLATION => [ - [2292], - ], - self::EXCEPTION_INVALID_FIELD_NAME => [ - [904], - ], - self::EXCEPTION_NON_UNIQUE_FIELD_NAME => [ - [918], - [960], - ], - self::EXCEPTION_NOT_NULL_CONSTRAINT_VIOLATION => [ - [1400], - ], - self::EXCEPTION_SYNTAX_ERROR => [ - [923], - ], - self::EXCEPTION_TABLE_EXISTS => [ - [955], - ], - self::EXCEPTION_TABLE_NOT_FOUND => [ - [942], - ], - self::EXCEPTION_UNIQUE_CONSTRAINT_VIOLATION => [ - [1], - [2299], - [38911], - ], - ]; + return new OCI\ExceptionConverter(); } } diff --git a/tests/Driver/AbstractPostgreSQLDriverTest.php b/tests/Driver/AbstractPostgreSQLDriverTest.php index 9c027190cf5..ecf2d30c022 100644 --- a/tests/Driver/AbstractPostgreSQLDriverTest.php +++ b/tests/Driver/AbstractPostgreSQLDriverTest.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\PostgreSQL; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\PostgreSQL100Platform; use Doctrine\DBAL\Platforms\PostgreSQL94Platform; @@ -28,6 +30,11 @@ protected function createSchemaManager(Connection $connection): AbstractSchemaMa return new PostgreSqlSchemaManager($connection); } + protected function createExceptionConverter(): ExceptionConverter + { + return new PostgreSQL\ExceptionConverter(); + } + /** * {@inheritDoc} */ @@ -40,44 +47,4 @@ protected function getDatabasePlatformsForVersions(): array ['10', PostgreSQL100Platform::class], ]; } - - /** - * {@inheritDoc} - */ - protected static function getExceptionConversionData(): array - { - return [ - self::EXCEPTION_CONNECTION => [ - [7, null, 'SQLSTATE[08006]'], - ], - self::EXCEPTION_FOREIGN_KEY_CONSTRAINT_VIOLATION => [ - [0, '23503'], - ], - self::EXCEPTION_INVALID_FIELD_NAME => [ - [0, '42703'], - ], - self::EXCEPTION_NON_UNIQUE_FIELD_NAME => [ - [0, '42702'], - ], - self::EXCEPTION_NOT_NULL_CONSTRAINT_VIOLATION => [ - [0, '23502'], - ], - self::EXCEPTION_SYNTAX_ERROR => [ - [0, '42601'], - ], - self::EXCEPTION_TABLE_EXISTS => [ - [0, '42P07'], - ], - self::EXCEPTION_TABLE_NOT_FOUND => [ - [0, '42P01'], - ], - self::EXCEPTION_UNIQUE_CONSTRAINT_VIOLATION => [ - [0, '23505'], - ], - self::EXCEPTION_DEADLOCK => [ - [0, '40001'], - [0, '40P01'], - ], - ]; - } } diff --git a/tests/Driver/AbstractSQLServerDriverTest.php b/tests/Driver/AbstractSQLServerDriverTest.php index 4ee94197a71..4ef2a2c1bd5 100644 --- a/tests/Driver/AbstractSQLServerDriverTest.php +++ b/tests/Driver/AbstractSQLServerDriverTest.php @@ -4,6 +4,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception\PortWithoutHost; +use Doctrine\DBAL\Driver\API\DefaultExceptionConverter; +use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\SQLServer2012Platform; use Doctrine\DBAL\Schema\AbstractSchemaManager; @@ -21,6 +23,11 @@ protected function createSchemaManager(Connection $connection): AbstractSchemaMa return new SQLServerSchemaManager($connection); } + protected function createExceptionConverter(): ExceptionConverter + { + return new DefaultExceptionConverter(); + } + /** * {@inheritDoc} */ diff --git a/tests/Driver/AbstractSQLiteDriverTest.php b/tests/Driver/AbstractSQLiteDriverTest.php index c12b3342af7..ba44642a75f 100644 --- a/tests/Driver/AbstractSQLiteDriverTest.php +++ b/tests/Driver/AbstractSQLiteDriverTest.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\AbstractSQLiteDriver; +use Doctrine\DBAL\Driver\API\ExceptionConverter; +use Doctrine\DBAL\Driver\API\SQLite; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Schema\AbstractSchemaManager; @@ -27,44 +29,8 @@ protected function createSchemaManager(Connection $connection): AbstractSchemaMa return new SqliteSchemaManager($connection); } - /** - * {@inheritDoc} - */ - protected static function getExceptionConversionData(): array + protected function createExceptionConverter(): ExceptionConverter { - return [ - self::EXCEPTION_CONNECTION => [ - [0, null, 'unable to open database file'], - ], - self::EXCEPTION_INVALID_FIELD_NAME => [ - [0, null, 'has no column named'], - ], - self::EXCEPTION_NON_UNIQUE_FIELD_NAME => [ - [0, null, 'ambiguous column name'], - ], - self::EXCEPTION_NOT_NULL_CONSTRAINT_VIOLATION => [ - [0, null, 'may not be NULL'], - ], - self::EXCEPTION_READ_ONLY => [ - [0, null, 'attempt to write a readonly database'], - ], - self::EXCEPTION_SYNTAX_ERROR => [ - [0, null, 'syntax error'], - ], - self::EXCEPTION_TABLE_EXISTS => [ - [0, null, 'already exists'], - ], - self::EXCEPTION_TABLE_NOT_FOUND => [ - [0, null, 'no such table:'], - ], - self::EXCEPTION_UNIQUE_CONSTRAINT_VIOLATION => [ - [0, null, 'must be unique'], - [0, null, 'is not unique'], - [0, null, 'are not unique'], - ], - self::EXCEPTION_LOCK_WAIT_TIMEOUT => [ - [0, null, 'database is locked'], - ], - ]; + return new SQLite\ExceptionConverter(); } } diff --git a/tests/Functional/DataAccessTest.php b/tests/Functional/DataAccessTest.php index c14d2399892..dac10079749 100644 --- a/tests/Functional/DataAccessTest.php +++ b/tests/Functional/DataAccessTest.php @@ -4,10 +4,6 @@ use DateTime; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Driver\IBMDB2\Driver as IBMDB2Driver; -use Doctrine\DBAL\Driver\Mysqli\Driver as MySQLiDriver; -use Doctrine\DBAL\Driver\SQLSrv\Driver as SQLSrvDriver; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Platforms\TrimMode; @@ -190,66 +186,6 @@ public function testFetchAllWithTypes(): void self::assertStringStartsWith($datetimeString, $row['test_datetime']); } - /** - * @group DBAL-209 - * @dataProvider fetchProvider - */ - public function testFetchAllWithMissingTypes(callable $fetch): void - { - if ( - $this->connection->getDriver() instanceof MySQLiDriver || - $this->connection->getDriver() instanceof SQLSrvDriver - ) { - self::markTestSkipped('mysqli and sqlsrv actually supports this'); - } - - if ( - $this->connection->getDriver() instanceof IBMDB2Driver - ) { - $this->markTestSkipped( - 'ibm_ibm2 may or may not report the error depending on the PHP version and the connection state' - ); - } - - $datetimeString = '2010-01-01 10:10:10'; - $datetime = new DateTime($datetimeString); - $sql = 'SELECT test_int, test_datetime FROM fetch_table WHERE test_int = ? AND test_datetime = ?'; - - $this->expectException(DBALException::class); - - $fetch($this->connection, $sql, [1, $datetime]); - } - - /** - * @return iterable - */ - public static function fetchProvider(): iterable - { - yield 'fetch-all-associative' => [ - static function (Connection $connection, string $query, array $params): void { - $connection->fetchAllAssociative($query, $params); - }, - ]; - - yield 'fetch-numeric' => [ - static function (Connection $connection, string $query, array $params): void { - $connection->fetchNumeric($query, $params); - }, - ]; - - yield 'fetch-associative' => [ - static function (Connection $connection, string $query, array $params): void { - $connection->fetchAssociative($query, $params); - }, - ]; - - yield 'fetch-one' => [ - static function (Connection $connection, string $query, array $params): void { - $connection->fetchOne($query, $params); - }, - ]; - } - public function testFetchNoResult(): void { self::assertFalse( diff --git a/tests/StatementTest.php b/tests/StatementTest.php index 13146764ca4..da2579ba8b3 100644 --- a/tests/StatementTest.php +++ b/tests/StatementTest.php @@ -7,11 +7,11 @@ use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\Connection as DriverConnection; +use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Driver\Statement as DriverStatement; use Doctrine\DBAL\Logging\SQLLogger; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Statement; -use Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -127,10 +127,6 @@ public function testExecuteCallsLoggerStopQueryOnException(): void ->method('getSQLLogger') ->will(self::returnValue($logger)); - $this->conn->expects(self::any()) - ->method('handleExceptionDuringQuery') - ->will(self::throwException(new DBALException())); - $logger->expects(self::once()) ->method('startQuery'); @@ -139,7 +135,9 @@ public function testExecuteCallsLoggerStopQueryOnException(): void $this->driverStatement->expects(self::once()) ->method('execute') - ->will(self::throwException(new Exception('Mock test exception'))); + ->will(self::throwException( + $this->createMock(DriverException::class) + )); $statement = new Statement('', $this->conn);