Skip to content

Commit 07e815b

Browse files
authored
feat: add UniqueConstraintViolationException and getLastException() (#9979)
1 parent b0be475 commit 07e815b

File tree

18 files changed

+549
-42
lines changed

18 files changed

+549
-42
lines changed

rector.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
ClassPropertyAssignToConstructorPromotionRector::class => [
130130
__DIR__ . '/system/Database/BaseResult.php',
131131
__DIR__ . '/system/Database/RawSql.php',
132+
__DIR__ . '/system/Database/Exceptions/DatabaseException.php',
132133
__DIR__ . '/system/Debug/BaseExceptionHandler.php',
133134
__DIR__ . '/system/Debug/Exceptions.php',
134135
__DIR__ . '/system/Filters/Filters.php',

system/Database/BaseConnection.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ abstract class BaseConnection implements ConnectionInterface
219219
*/
220220
protected $lastQuery;
221221

222+
/**
223+
* The exception that would have been thrown on the last failed query
224+
* if DBDebug were enabled. Null when the last query succeeded or when
225+
* DBDebug is true (in which case the exception is thrown directly and
226+
* this property is never set).
227+
*/
228+
protected ?DatabaseException $lastException = null;
229+
222230
/**
223231
* Connection ID
224232
*
@@ -698,8 +706,9 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s
698706

699707
// Run the query for real
700708
try {
701-
$exception = null;
702-
$this->resultID = $this->simpleQuery($query->getQuery());
709+
$exception = null;
710+
$this->lastException = null;
711+
$this->resultID = $this->simpleQuery($query->getQuery());
703712
} catch (DatabaseException $exception) {
704713
$this->resultID = false;
705714
}
@@ -737,11 +746,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s
737746
Events::trigger('DBQuery', $query);
738747

739748
if ($exception instanceof DatabaseException) {
740-
throw new DatabaseException(
741-
$exception->getMessage(),
742-
$exception->getCode(),
743-
$exception,
744-
);
749+
throw $exception;
745750
}
746751

747752
return false;
@@ -1847,6 +1852,17 @@ public function isWriteType($sql): bool
18471852
*/
18481853
abstract public function error(): array;
18491854

1855+
/**
1856+
* Returns the exception that would have been thrown on the last failed
1857+
* query if DBDebug were enabled. Returns null if the last query succeeded
1858+
* or if DBDebug is true (in which case the exception is always thrown
1859+
* directly and this method will always return null).
1860+
*/
1861+
public function getLastException(): ?DatabaseException
1862+
{
1863+
return $this->lastException;
1864+
}
1865+
18501866
/**
18511867
* Insert ID
18521868
*

system/Database/Exceptions/DatabaseException.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,34 @@
1515

1616
use CodeIgniter\Exceptions\HasExitCodeInterface;
1717
use CodeIgniter\Exceptions\RuntimeException;
18+
use Throwable;
1819

1920
class DatabaseException extends RuntimeException implements ExceptionInterface, HasExitCodeInterface
2021
{
22+
/**
23+
* Native code returned by the database driver.
24+
*/
25+
protected int|string $databaseCode = 0;
26+
27+
/**
28+
* @param int|string $code Native database code (e.g. 1062, 23505, 23000/2601)
29+
*/
30+
public function __construct(string $message = '', int|string $code = 0, ?Throwable $previous = null)
31+
{
32+
$this->databaseCode = $code;
33+
34+
// Keep Throwable::getCode() behavior BC-friendly for non-int DB codes.
35+
parent::__construct($message, is_int($code) ? $code : 0, $previous);
36+
}
37+
38+
/**
39+
* Returns the native code from the database driver.
40+
*/
41+
public function getDatabaseCode(): int|string
42+
{
43+
return $this->databaseCode;
44+
}
45+
2146
public function getExitCode(): int
2247
{
2348
return EXIT_DATABASE;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Database\Exceptions;
15+
16+
class UniqueConstraintViolationException extends DatabaseException
17+
{
18+
}

system/Database/MySQLi/Connection.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use CodeIgniter\Database\BaseConnection;
1717
use CodeIgniter\Database\Exceptions\DatabaseException;
18+
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
1819
use CodeIgniter\Database\TableName;
1920
use CodeIgniter\Exceptions\LogicException;
2021
use mysqli;
@@ -317,9 +318,16 @@ protected function execute(string $sql)
317318
'trace' => render_backtrace($e->getTrace()),
318319
]);
319320

321+
// MySQL error 1062: ER_DUP_ENTRY – duplicate key value
322+
$exception = $e->getCode() === 1062
323+
? new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e)
324+
: new DatabaseException($e->getMessage(), $e->getCode(), $e);
325+
320326
if ($this->DBDebug) {
321-
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
327+
throw $exception;
322328
}
329+
330+
$this->lastException = $exception;
323331
}
324332

325333
return false;

system/Database/OCI8/Connection.php

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use CodeIgniter\Database\BaseConnection;
1717
use CodeIgniter\Database\Exceptions\DatabaseException;
18+
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
1819
use CodeIgniter\Database\Query;
1920
use CodeIgniter\Database\TableName;
2021
use ErrorException;
@@ -236,10 +237,27 @@ protected function execute(string $sql)
236237

237238
oci_set_prefetch($this->stmtId, 1000);
238239

239-
$result = oci_execute($this->stmtId, $this->commitMode) ? $this->stmtId : false;
240+
$result = oci_execute($this->stmtId, $this->commitMode) ? $this->stmtId : false;
241+
242+
if ($result === false) {
243+
// ORA-00001: unique constraint violated
244+
$error = $this->error();
245+
$exception = $error['code'] === 1
246+
? new UniqueConstraintViolationException((string) $error['message'], $error['code'])
247+
: new DatabaseException((string) $error['message'], $error['code']);
248+
249+
if ($this->DBDebug) {
250+
throw $exception;
251+
}
252+
253+
$this->lastException = $exception;
254+
255+
return false;
256+
}
257+
240258
$insertTableName = $this->parseInsertTableName($sql);
241259

242-
if ($result && $insertTableName !== '') {
260+
if ($insertTableName !== '') {
243261
$this->lastInsertedTableName = $insertTableName;
244262
}
245263

@@ -254,9 +272,17 @@ protected function execute(string $sql)
254272
'trace' => render_backtrace($trace),
255273
]);
256274

275+
// ORA-00001: unique constraint violated
276+
$error = $this->error();
277+
$exception = $error['code'] === 1
278+
? new UniqueConstraintViolationException((string) $error['message'], $error['code'], $e)
279+
: new DatabaseException((string) $error['message'], $error['code'], $e);
280+
257281
if ($this->DBDebug) {
258-
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
282+
throw $exception;
259283
}
284+
285+
$this->lastException = $exception;
260286
}
261287

262288
return false;
@@ -615,25 +641,25 @@ protected function bindParams($params)
615641
*/
616642
public function error(): array
617643
{
618-
// oci_error() returns an array that already contains
619-
// 'code' and 'message' keys, but it can return false
620-
// if there was no error ....
621-
$error = oci_error();
644+
// oci_error() is resource-specific: check each resource in priority order
645+
// and return the first one that actually has an error. This ensures that
646+
// e.g. oci_parse() failures (error on connID) are found even when stmtId
647+
// holds a stale valid resource from the previous successful query.
622648
$resources = [$this->cursorId, $this->stmtId, $this->connID];
623649

624650
foreach ($resources as $resource) {
625651
if (is_resource($resource)) {
626652
$error = oci_error($resource);
627-
break;
653+
654+
if (is_array($error)) {
655+
return $error;
656+
}
628657
}
629658
}
630659

631-
return is_array($error)
632-
? $error
633-
: [
634-
'code' => '',
635-
'message' => '',
636-
];
660+
$error = oci_error();
661+
662+
return is_array($error) ? $error : ['code' => '', 'message' => ''];
637663
}
638664

639665
public function insertID(): int

0 commit comments

Comments
 (0)