Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion system/Database/BaseConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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,
Expand Down
48 changes: 44 additions & 4 deletions system/Database/BasePreparedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\BadMethodCallException;
use ErrorException;
use Throwable;

/**
* @template TConnection
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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.
*
Expand Down
22 changes: 18 additions & 4 deletions system/Database/MySQLi/PreparedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions system/Database/OCI8/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ 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;
}

/**
* Checks whether the native database code represents a retryable transaction failure.
*/
protected function isRetryableTransactionErrorCode(int|string $code): bool
{
return in_array($code, [60, 8177], true);
return in_array((int) $code, [60, 8177], true);
}

/**
Expand Down
40 changes: 36 additions & 4 deletions system/Database/OCI8/PreparedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CodeIgniter\Database\BasePreparedQuery;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Exceptions\BadMethodCallException;
use ErrorException;
use OCILob;

/**
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 !== '') {
Expand Down Expand Up @@ -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.
Expand Down
49 changes: 45 additions & 4 deletions system/Database/Postgre/PreparedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions system/Database/SQLSRV/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 15 additions & 5 deletions system/Database/SQLSRV/PreparedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}

Expand All @@ -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;
Expand Down
Loading
Loading