diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index cd497447f71e..dad13b4a394a 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -2140,6 +2140,16 @@ public function getLastException(): ?DatabaseException return $this->lastException; } + /** + * Sets the exception for the last failed database operation. + * + * @internal This method is for internal database component use only. + */ + public function setLastException(?DatabaseException $exception): void + { + $this->lastException = $exception; + } + /** * Checks whether the native database error represents a unique constraint violation. */ @@ -2158,8 +2168,10 @@ protected function isRetryableTransactionErrorCode(int|string $code): bool /** * Creates the appropriate database exception for a native database error. + * + * @internal This method is for internal database component use only. */ - protected function createDatabaseException( + public function createDatabaseException( string $message, int|string $code = 0, ?Throwable $previous = null, diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index f2ee6b3ed6a4..b420c8ce72bf 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -18,6 +18,7 @@ use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\BadMethodCallException; use ErrorException; +use Throwable; /** * @template TConnection @@ -49,6 +50,11 @@ abstract class BasePreparedQuery implements PreparedQueryInterface */ protected $errorString; + /** + * The typed exception for the last failed prepared query, if any. + */ + protected ?DatabaseException $databaseException = null; + /** * Holds the prepared query object * that is cloned during execute. @@ -121,8 +127,10 @@ public function execute(...$data) try { $exception = null; - $result = $this->_execute($data); - } catch (ArgumentCountError|ErrorException $exception) { + $this->db->setLastException(null); + $this->databaseException = null; + $result = $this->_execute($data); + } catch (ArgumentCountError|DatabaseException|ErrorException $exception) { $result = false; } @@ -136,6 +144,8 @@ public function execute(...$data) // This will trigger a rollback if transactions are being used $this->db->handleTransStatus(); + $databaseException = $this->createDatabaseException($exception); + if ($this->db->DBDebug) { // We call this function in order to roll-back queries // if transactions are enabled. If we don't call this here @@ -154,8 +164,8 @@ public function execute(...$data) // Let others do something with this query. Events::trigger('DBQuery', $query); - if ($exception !== null) { - throw new DatabaseException($exception->getMessage(), $exception->getCode(), $exception); + if ($databaseException instanceof DatabaseException) { + throw $databaseException; } return false; @@ -164,6 +174,8 @@ public function execute(...$data) // Let others do something with this query. Events::trigger('DBQuery', $query); + $this->db->setLastException($databaseException); + return false; } @@ -196,6 +208,34 @@ abstract public function _execute(array $data): bool; */ abstract public function _getResult(); + /** + * Creates the database exception for a failed prepared query. + */ + private function createDatabaseException(?Throwable $previous): ?DatabaseException + { + if ($previous instanceof DatabaseException) { + return $previous; + } + + if ($this->databaseException instanceof DatabaseException) { + return $this->databaseException; + } + + if ($previous instanceof Throwable) { + return $this->db->createDatabaseException( + $previous->getMessage(), + $previous->getCode(), + $previous, + ); + } + + if ($this->errorString === null || $this->errorString === '') { + return null; + } + + return $this->db->createDatabaseException($this->errorString, $this->errorCode); + } + /** * Explicitly closes the prepared statement. * diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 05ef5f151b7b..05ce7595574d 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Database\MySQLi; use CodeIgniter\Database\BasePreparedQuery; -use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Exceptions\BadMethodCallException; use mysqli; use mysqli_result; @@ -49,7 +48,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery $this->errorString = $this->db->mysqli->error; if ($this->db->DBDebug) { - throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode); + throw $this->db->createDatabaseException($this->errorString, $this->errorCode); } } @@ -93,14 +92,29 @@ public function _execute(array $data): bool } try { - return $this->statement->execute(); + $result = $this->statement->execute(); } catch (mysqli_sql_exception $e) { + $this->errorCode = $e->getCode(); + $this->errorString = $e->getMessage(); + $this->databaseException = $this->db->createDatabaseException($this->errorString, $this->errorCode, $e); + if ($this->db->DBDebug) { - throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + throw $this->databaseException; } return false; } + + if ($result === false) { + $this->errorCode = $this->statement->errno; + $this->errorString = $this->statement->error; + + if ($this->db->DBDebug) { + throw $this->db->createDatabaseException($this->errorString, $this->errorCode); + } + } + + return $result; } /** diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 8ed4406c7d11..1fcbf9e74824 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -120,7 +120,7 @@ class Connection extends BaseConnection protected function isUniqueConstraintViolation(int|string $code, string $message): bool { // ORA-00001: unique constraint violated. - return $code === 1; + return (int) $code === 1; } /** @@ -128,7 +128,7 @@ protected function isUniqueConstraintViolation(int|string $code, string $message */ protected function isRetryableTransactionErrorCode(int|string $code): bool { - return in_array($code, [60, 8177], true); + return in_array((int) $code, [60, 8177], true); } /** diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index 390ed938fc1c..c7ca22543bc6 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -16,6 +16,7 @@ use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Exceptions\BadMethodCallException; +use ErrorException; use OCILob; /** @@ -55,7 +56,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery $this->errorString = $error['message'] ?? ''; if ($this->db->DBDebug) { - throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode); + throw $this->db->createDatabaseException($this->errorString, $this->errorCode); } } @@ -86,10 +87,28 @@ public function _execute(array $data): bool } } - $result = oci_execute($this->statement, $this->db->commitMode); + try { + $result = oci_execute($this->statement, $this->db->commitMode); + } catch (ErrorException $e) { + $databaseException = $this->setDatabaseExceptionFromStatement($e); - if ($binaryData instanceof OCILob) { - $binaryData->free(); + if ($this->db->DBDebug) { + throw $databaseException; + } + + return false; + } finally { + if ($binaryData instanceof OCILob) { + $binaryData->free(); + } + } + + if ($result === false) { + $databaseException = $this->setDatabaseExceptionFromStatement(); + + if ($this->db->DBDebug) { + throw $databaseException; + } } if ($result && $this->lastInsertTableName !== '') { @@ -117,6 +136,19 @@ protected function _close(): bool return oci_free_statement($this->statement); } + /** + * Captures the native OCI statement error for shared database exception classification. + */ + private function setDatabaseExceptionFromStatement(?ErrorException $previous = null): DatabaseException + { + $error = oci_error($this->statement); + $this->errorCode = $error['code'] ?? 0; + $this->errorString = $error['message'] ?? $previous?->getMessage() ?? ''; + $this->databaseException = $this->db->createDatabaseException($this->errorString, $this->errorCode, $previous); + + return $this->databaseException; + } + /** * Replaces the ? placeholders with :0, :1, etc parameters for use * within the prepared query. diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 08900a6ddcfd..775d10a12b34 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Database\Postgre; use CodeIgniter\Database\BasePreparedQuery; -use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Exceptions\BadMethodCallException; use Exception; use PgSql\Connection as PgSqlConnection; @@ -70,7 +69,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery $this->errorString = pg_last_error($this->db->connID); if ($this->db->DBDebug) { - throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode); + throw $this->db->createDatabaseException($this->errorString, $this->errorCode); } } @@ -93,9 +92,51 @@ public function _execute(array $data): bool } } - $this->result = pg_execute($this->db->connID, $this->name, $data); + $sent = pg_send_execute($this->db->connID, $this->name, $data); - return (bool) $this->result; + if ($sent === false || $sent === 0) { + $this->errorCode = 0; + $this->errorString = pg_last_error($this->db->connID); + + return false; + } + + $this->result = pg_get_result($this->db->connID); + + if ($this->result === false) { + $this->errorCode = 0; + $this->errorString = pg_last_error($this->db->connID); + + return false; + } + + $lastResult = $this->result; + $failedResult = pg_result_status($this->result) === PGSQL_FATAL_ERROR ? $this->result : null; + + while (($next = pg_get_result($this->db->connID)) !== false) { + $lastResult = $next; + + if (! $failedResult instanceof PgSqlResult && pg_result_status($next) === PGSQL_FATAL_ERROR) { + $failedResult = $next; + } + } + + $this->result = $lastResult; + + if ($failedResult instanceof PgSqlResult) { + $sqlstate = (string) pg_result_error_field($failedResult, PGSQL_DIAG_SQLSTATE); + $this->errorCode = 0; + $this->errorString = (string) pg_result_error($failedResult); + $this->databaseException = $this->db->createDatabaseException($this->errorString, $sqlstate); + + if ($this->db->DBDebug) { + throw $this->databaseException; + } + + return false; + } + + return true; } /** diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 813a4d49e2a5..d698a8f9d644 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -94,6 +94,16 @@ class Connection extends BaseConnection */ protected function isUniqueConstraintViolation(int|string $code, string $message): bool { + $code = (string) $code; + + if (str_contains($code, '/')) { + [$sqlstate, $vendorCode] = explode('/', $code, 2); + + if ($sqlstate === '23000' && in_array((int) $vendorCode, [2627, 2601], true)) { + return true; + } + } + $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); if (! is_array($errors)) { return false; diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index 19d2d5adfe6b..9336e231dd2c 100644 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -65,12 +65,14 @@ public function _prepare(string $sql, array $options = []): PreparedQuery $this->statement = sqlsrv_prepare($this->db->connID, $sql, $parameters); if (! $this->statement) { + $info = $this->db->error(); + $this->databaseException = $this->db->createDatabaseException($this->db->getAllErrorMessages(), $info['code']); + if ($this->db->DBDebug) { - throw new DatabaseException($this->db->getAllErrorMessages()); + throw $this->databaseException; } - $info = $this->db->error(); - $this->errorCode = $info['code']; + $this->errorCode = is_int($info['code']) ? $info['code'] : 0; $this->errorString = $info['message']; } @@ -93,8 +95,16 @@ public function _execute(array $data): bool $result = sqlsrv_execute($this->statement); - if ($result === false && $this->db->DBDebug) { - throw new DatabaseException($this->db->getAllErrorMessages()); + if ($result === false) { + $error = $this->db->error(); + + $this->errorCode = is_int($error['code']) ? $error['code'] : 0; + $this->errorString = $this->db->getAllErrorMessages(); + $this->databaseException = $this->db->createDatabaseException($this->errorString, $error['code']); + + if ($this->db->DBDebug) { + throw $this->databaseException; + } } return $result; diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index afd2669c6436..d52bae99f06a 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Database\SQLite3; use CodeIgniter\Database\BasePreparedQuery; -use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Exceptions\BadMethodCallException; use Exception; use SQLite3; @@ -52,7 +51,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery $this->errorString = $this->db->connID->lastErrorMsg(); if ($this->db->DBDebug) { - throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode); + throw $this->db->createDatabaseException($this->errorString, $this->errorCode); } } @@ -88,13 +87,27 @@ public function _execute(array $data): bool try { $this->result = $this->statement->execute(); } catch (Exception $e) { + $error = $this->db->error(); + $this->errorCode = $error['code']; + $this->errorString = $e->getMessage(); + $this->databaseException = $this->db->createDatabaseException($this->errorString, $this->errorCode, $e); + if ($this->db->DBDebug) { - throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + throw $this->databaseException; } return false; } + if ($this->result === false) { + $this->errorCode = $this->db->connID->lastErrorCode(); + $this->errorString = $this->db->connID->lastErrorMsg(); + + if ($this->db->DBDebug) { + throw $this->db->createDatabaseException($this->errorString, $this->errorCode); + } + } + return $this->result !== false; } diff --git a/tests/_support/Mock/MockPreparedQuery.php b/tests/_support/Mock/MockPreparedQuery.php index c31dc266d84d..93c29ea73247 100644 --- a/tests/_support/Mock/MockPreparedQuery.php +++ b/tests/_support/Mock/MockPreparedQuery.php @@ -14,6 +14,7 @@ namespace Tests\Support\Mock; use CodeIgniter\Database\BasePreparedQuery; +use Throwable; /** * @internal @@ -22,7 +23,8 @@ */ final class MockPreparedQuery extends BasePreparedQuery { - public string $preparedSql = ''; + public string $preparedSql = ''; + public ?Throwable $thrownException = null; /** * @param array $options @@ -39,6 +41,10 @@ public function _prepare(string $sql, array $options = []): self */ public function _execute(array $data): bool { + if ($this->thrownException instanceof Throwable) { + throw $this->thrownException; + } + return true; } diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index 304269969a2d..6d8b1d1eecf2 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Database\BasePreparedQuery; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException; use CodeIgniter\Database\Query; use CodeIgniter\Database\ResultInterface; use CodeIgniter\Exceptions\BadMethodCallException; @@ -222,14 +223,33 @@ public function testExecuteRunsQueryAndReturnsFalse(): void $this->disableDBDebug(); $this->assertTrue($this->query->execute('foo1', 'bar')); + $this->assertNotInstanceOf(DatabaseException::class, $this->db->getLastException()); + $this->assertFalse($this->query->execute('foo1', 'baz')); + $exception = $this->db->getLastException(); + $this->assertInstanceOf(UniqueConstraintViolationException::class, $exception); + $this->enableDBDebug(); $this->seeInDatabase($this->db->DBPrefix . 'without_auto_increment', ['key' => 'foo1', 'value' => 'bar']); $this->dontSeeInDatabase($this->db->DBPrefix . 'without_auto_increment', ['key' => 'foo1', 'value' => 'baz']); } + public function testExecuteThrowsUniqueConstraintViolationException(): void + { + $this->query = $this->db->prepare(static fn ($db) => $db->table('without_auto_increment')->insert([ + 'key' => 'a', + 'value' => 'b', + ])); + + $this->assertTrue($this->query->execute('foo1', 'bar')); + + $this->expectException(UniqueConstraintViolationException::class); + + $this->query->execute('foo1', 'baz'); + } + public function testExecuteRunsQueryManualAndReturnsFalse(): void { $this->query = $this->db->prepare(static function ($db): Query { diff --git a/tests/system/Database/RetryableTransactionExceptionTest.php b/tests/system/Database/RetryableTransactionExceptionTest.php index 1276832463b0..a78f15d20142 100644 --- a/tests/system/Database/RetryableTransactionExceptionTest.php +++ b/tests/system/Database/RetryableTransactionExceptionTest.php @@ -21,11 +21,13 @@ use CodeIgniter\Database\Postgre\Connection as PostgreConnection; use CodeIgniter\Database\SQLite3\Connection as SQLite3Connection; use CodeIgniter\Database\SQLSRV\Connection as SQLSRVConnection; +use CodeIgniter\Events\Events; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use ErrorException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use ReflectionMethod; +use Tests\Support\Mock\MockPreparedQuery; /** * @internal @@ -74,12 +76,30 @@ public static function provideCreatesUniqueConstraintViolationExceptions(): iter 'column email is not unique', ]; + yield 'SQLSRV unique constraint' => [ + self::connection(SQLSRVConnection::class, 'SQLSRV'), + '23000/2627', + 'Violation of UNIQUE KEY constraint.', + ]; + + yield 'SQLSRV unique index' => [ + self::connection(SQLSRVConnection::class, 'SQLSRV'), + '23000/2601', + 'Cannot insert duplicate key row.', + ]; + if (defined('OCI_COMMIT_ON_SUCCESS')) { yield 'OCI8 unique constraint' => [ self::connection(OCI8Connection::class, 'OCI8'), 1, 'Unique constraint violated.', ]; + + yield 'OCI8 unique constraint string code' => [ + self::connection(OCI8Connection::class, 'OCI8'), + '1', + 'Unique constraint violated.', + ]; } } @@ -114,7 +134,11 @@ public static function provideCreatesRetryableTransactionExceptions(): iterable if (defined('OCI_COMMIT_ON_SUCCESS')) { yield 'OCI8 deadlock' => [self::connection(OCI8Connection::class, 'OCI8'), 60]; + yield 'OCI8 deadlock string code' => [self::connection(OCI8Connection::class, 'OCI8'), '60']; + yield 'OCI8 serialization failure' => [self::connection(OCI8Connection::class, 'OCI8'), 8177]; + + yield 'OCI8 serialization failure string code' => [self::connection(OCI8Connection::class, 'OCI8'), '8177']; } } @@ -179,6 +203,56 @@ public function testQueryThrowsRetryableTransactionExceptionFromDriverExecutionP $db->query('SELECT * FROM test'); } + public function testPreparedQueryThrowsRetryableTransactionExceptionFromBaseExecutionPath(): void + { + $preparedQuery = new MockPreparedQuery(self::connection(MySQLiConnection::class, 'MySQLi')); + $preparedQuery->thrownException = new ErrorException('Deadlock found when trying to get lock.', 1213); + + $preparedQuery->prepare('SELECT 1'); + + $this->expectException(RetryableTransactionException::class); + + $preparedQuery->execute(); + } + + public function testPreparedQueryRoutesDriverDatabaseExceptionThroughBaseExecutionPath(): void + { + $db = self::connection(MySQLiConnection::class, 'MySQLi'); + $preparedQuery = new MockPreparedQuery($db); + $preparedQuery->thrownException = self::createDatabaseException($db, 'Deadlock found when trying to get lock.', 1213); + $queryCount = 0; + $listener = static function () use (&$queryCount): void { + $queryCount++; + }; + + $preparedQuery->prepare('SELECT 1'); + Events::on('DBQuery', $listener); + + try { + $preparedQuery->execute(); + $this->fail('Expected retryable transaction exception was not thrown.'); + } catch (RetryableTransactionException $e) { + $this->assertSame($preparedQuery->thrownException, $e); + } finally { + Events::removeListener('DBQuery', $listener); + } + + $this->assertSame(1, $queryCount); + } + + public function testPreparedQueryStoresRetryableTransactionExceptionWithDebugDisabled(): void + { + $db = new MySQLiConnection(self::config('MySQLi', false)); + + $preparedQuery = new MockPreparedQuery($db); + $preparedQuery->thrownException = new ErrorException('Deadlock found when trying to get lock.', 1213); + + $preparedQuery->prepare('SELECT 1'); + + $this->assertFalse($preparedQuery->execute()); + $this->assertInstanceOf(RetryableTransactionException::class, $db->getLastException()); + } + /** * @param class-string $connectionClass */ @@ -190,7 +264,7 @@ private static function connection(string $connectionClass, string $driver): Bas /** * @return array */ - private static function config(string $driver): array + private static function config(string $driver, bool $debug = true): array { return [ 'DSN' => '', @@ -199,7 +273,7 @@ private static function config(string $driver): array 'password' => '', 'database' => 'test', 'DBDriver' => $driver, - 'DBDebug' => true, + 'DBDebug' => $debug, 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', @@ -209,13 +283,8 @@ private static function config(string $driver): array ]; } - private static function createDatabaseException( - BaseConnection $db, - string $message, - int|string $code, - ): DatabaseException { - $method = new ReflectionMethod($db, 'createDatabaseException'); - - return $method->invoke($db, $message, $code); + private static function createDatabaseException(BaseConnection $db, string $message, int|string $code): DatabaseException + { + return $db->createDatabaseException($message, $code); } } diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 9efb8fd61ea8..4ced02dcf66a 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -203,6 +203,7 @@ Database - Added ``afterCommit()`` and ``afterRollback()`` transaction callbacks to database connections. These callbacks run after the outermost transaction commits or rolls back. See :ref:`transactions-transaction-callbacks`. - Added ``inTransaction()`` to database connections to check whether the connection is inside an active CodeIgniter-managed transaction. See :ref:`transactions-checking-transaction-state`. +- Prepared query execution failures now throw or store typed database exceptions such as ``UniqueConstraintViolationException`` and ``RetryableTransactionException`` when applicable, matching normal query failures. - Added ``RetryableTransactionException`` for driver-specific retryable transaction failures such as deadlocks and serialization failures. See :ref:`transactions-retryable-exceptions`. - Added the ``transaction()`` method to database connections to run a callback inside a transaction. See :ref:`transactions-closure`. - Added ``trustServerCertificate`` option to ``SQLSRV`` database connections in ``Config\Database``. Set it to ``true`` to trust the server certificate without CA validation when using encrypted connections. diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 8109399ee25c..6e3f625d3010 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -225,15 +225,16 @@ UniqueConstraintViolationException .. versionadded:: 4.8.0 ``UniqueConstraintViolationException`` extends ``DatabaseException`` and is -thrown specifically when a query fails due to a duplicate key or unique -constraint violation. Catching it separately allows you to handle this case -without inspecting raw driver-specific error codes. +thrown specifically when a query or prepared query execution fails due to a +duplicate key or unique constraint violation. Catching it separately allows you +to handle this case without inspecting raw driver-specific error codes. DBDebug Disabled ================ -When ``DBDebug`` is ``false``, query failures return ``false`` instead of -throwing. Two methods are available to inspect what went wrong. +When ``DBDebug`` is ``false``, query and prepared query execution failures +return ``false`` instead of throwing. Two methods are available to inspect what +went wrong. $db->error() ------------ @@ -251,15 +252,16 @@ $db->getLastException() .. versionadded:: 4.8.0 -``getLastException()`` returns the typed exception that would have been -thrown had ``DBDebug`` been ``true``. This is the recommended way to -distinguish between failure types (e.g., a unique constraint violation vs. -another database error) without enabling ``DBDebug``: +``getLastException()`` returns the typed exception that would have been thrown +had ``DBDebug`` been ``true``. This is the recommended way to distinguish +between failure types (e.g., a unique constraint violation vs. another database +error) without enabling ``DBDebug``: .. literalinclude:: queries/031.php .. note:: ``getLastException()`` is reset to ``null`` at the start of every - query. Inspect it immediately after the failed operation. + query or prepared query execution. Inspect it immediately after the failed + operation. **************** Prepared Queries diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst index 1d3387d5768d..f98a1ec60473 100644 --- a/user_guide_src/source/database/transactions.rst +++ b/user_guide_src/source/database/transactions.rst @@ -105,10 +105,10 @@ side effects such as queued jobs, emails, cache invalidation, or external API calls, register them with ``afterCommit()`` so they run only after the transaction commits. -When ``DBDebug`` is ``false`` and a failed query returns ``false`` instead of -throwing, inspect ``getLastException()`` immediately after the failed operation. -It will contain the ``RetryableTransactionException`` instance when the driver -classifies the failure as retryable. +When ``DBDebug`` is ``false`` and a failed query or prepared query returns +``false`` instead of throwing, inspect ``getLastException()`` immediately after +the failed operation. It will contain the ``RetryableTransactionException`` +instance when the driver classifies the failure as retryable. Strict Mode ===========