Skip to content

Commit aee55a1

Browse files
committed
feat(database): add constraint violation exceptions
- Add a ConstraintViolationException hierarchy for typed integrity errors. - Classify foreign key, not-null, check, unique, and generic constraint violations per driver. - Keep getLastException() aligned with thrown exceptions when DBDebug is disabled. - Update database docs, examples, changelog, and focused tests. Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
1 parent 7b7742f commit aee55a1

18 files changed

Lines changed: 718 additions & 96 deletions

system/Database/BaseConnection.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515

1616
use BackedEnum;
1717
use Closure;
18+
use CodeIgniter\Database\Exceptions\CheckConstraintViolationException;
19+
use CodeIgniter\Database\Exceptions\ConstraintViolationException;
1820
use CodeIgniter\Database\Exceptions\DatabaseException;
21+
use CodeIgniter\Database\Exceptions\ForeignKeyConstraintViolationException;
22+
use CodeIgniter\Database\Exceptions\NotNullConstraintViolationException;
1923
use CodeIgniter\Database\Exceptions\RetryableTransactionException;
2024
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
2125
use CodeIgniter\Events\Events;
@@ -2231,6 +2235,38 @@ protected function isUniqueConstraintViolation(int|string $code, string $message
22312235
return false;
22322236
}
22332237

2238+
/**
2239+
* Checks whether the native database error represents a foreign key constraint violation.
2240+
*/
2241+
protected function isForeignKeyConstraintViolation(int|string $code, string $message): bool
2242+
{
2243+
return false;
2244+
}
2245+
2246+
/**
2247+
* Checks whether the native database error represents a NOT NULL constraint violation.
2248+
*/
2249+
protected function isNotNullConstraintViolation(int|string $code, string $message): bool
2250+
{
2251+
return false;
2252+
}
2253+
2254+
/**
2255+
* Checks whether the native database error represents a CHECK constraint violation.
2256+
*/
2257+
protected function isCheckConstraintViolation(int|string $code, string $message): bool
2258+
{
2259+
return false;
2260+
}
2261+
2262+
/**
2263+
* Checks whether the native database error represents a constraint violation.
2264+
*/
2265+
protected function isConstraintViolation(int|string $code, string $message): bool
2266+
{
2267+
return false;
2268+
}
2269+
22342270
/**
22352271
* Checks whether the native database code represents a retryable transaction failure.
22362272
*/
@@ -2253,6 +2289,22 @@ public function createDatabaseException(
22532289
return new UniqueConstraintViolationException($message, $code, $previous);
22542290
}
22552291

2292+
if ($this->isForeignKeyConstraintViolation($code, $message)) {
2293+
return new ForeignKeyConstraintViolationException($message, $code, $previous);
2294+
}
2295+
2296+
if ($this->isNotNullConstraintViolation($code, $message)) {
2297+
return new NotNullConstraintViolationException($message, $code, $previous);
2298+
}
2299+
2300+
if ($this->isCheckConstraintViolation($code, $message)) {
2301+
return new CheckConstraintViolationException($message, $code, $previous);
2302+
}
2303+
2304+
if ($this->isConstraintViolation($code, $message)) {
2305+
return new ConstraintViolationException($message, $code, $previous);
2306+
}
2307+
22562308
if ($this->isRetryableTransactionErrorCode($code)) {
22572309
return new RetryableTransactionException($message, $code, $previous);
22582310
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/**
17+
* Thrown when a CHECK constraint is violated.
18+
*/
19+
class CheckConstraintViolationException extends ConstraintViolationException
20+
{
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/**
17+
* Thrown when a database integrity constraint is violated.
18+
*/
19+
class ConstraintViolationException extends DatabaseException
20+
{
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/**
17+
* Thrown when a foreign key constraint is violated.
18+
*/
19+
class ForeignKeyConstraintViolationException extends ConstraintViolationException
20+
{
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/**
17+
* Thrown when a NOT NULL constraint is violated.
18+
*/
19+
class NotNullConstraintViolationException extends ConstraintViolationException
20+
{
21+
}

system/Database/Exceptions/UniqueConstraintViolationException.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace CodeIgniter\Database\Exceptions;
1515

16-
class UniqueConstraintViolationException extends DatabaseException
16+
/**
17+
* Thrown when a unique constraint is violated.
18+
*/
19+
class UniqueConstraintViolationException extends ConstraintViolationException
1720
{
1821
}

system/Database/MySQLi/Connection.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,33 @@ protected function isUniqueConstraintViolation(int|string $code, string $message
106106
return $code === 1062;
107107
}
108108

109+
/**
110+
* Checks whether the native database error represents a foreign key constraint violation.
111+
*/
112+
protected function isForeignKeyConstraintViolation(int|string $code, string $message): bool
113+
{
114+
// ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2.
115+
return in_array($code, [1451, 1452], true);
116+
}
117+
118+
/**
119+
* Checks whether the native database error represents a NOT NULL constraint violation.
120+
*/
121+
protected function isNotNullConstraintViolation(int|string $code, string $message): bool
122+
{
123+
// ER_BAD_NULL_ERROR: column cannot be null.
124+
return $code === 1048;
125+
}
126+
127+
/**
128+
* Checks whether the native database error represents a CHECK constraint violation.
129+
*/
130+
protected function isCheckConstraintViolation(int|string $code, string $message): bool
131+
{
132+
// ER_CHECK_CONSTRAINT_VIOLATED: check constraint is violated.
133+
return $code === 3819;
134+
}
135+
109136
/**
110137
* Checks whether the native database code represents a retryable transaction failure.
111138
*/

system/Database/OCI8/Connection.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,33 @@ protected function isUniqueConstraintViolation(int|string $code, string $message
123123
return (int) $code === 1;
124124
}
125125

126+
/**
127+
* Checks whether the native database error represents a foreign key constraint violation.
128+
*/
129+
protected function isForeignKeyConstraintViolation(int|string $code, string $message): bool
130+
{
131+
// ORA-02291: parent key not found; ORA-02292: child record found.
132+
return in_array((int) $code, [2291, 2292], true);
133+
}
134+
135+
/**
136+
* Checks whether the native database error represents a NOT NULL constraint violation.
137+
*/
138+
protected function isNotNullConstraintViolation(int|string $code, string $message): bool
139+
{
140+
// ORA-01400: cannot insert NULL.
141+
return (int) $code === 1400;
142+
}
143+
144+
/**
145+
* Checks whether the native database error represents a CHECK constraint violation.
146+
*/
147+
protected function isCheckConstraintViolation(int|string $code, string $message): bool
148+
{
149+
// ORA-02290: check constraint violated.
150+
return (int) $code === 2290;
151+
}
152+
126153
/**
127154
* Checks whether the native database code represents a retryable transaction failure.
128155
*/

system/Database/Postgre/Connection.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,38 @@ protected function isUniqueConstraintViolation(int|string $code, string $message
7171
return $code === '23505';
7272
}
7373

74+
/**
75+
* Checks whether the native database error represents a foreign key constraint violation.
76+
*/
77+
protected function isForeignKeyConstraintViolation(int|string $code, string $message): bool
78+
{
79+
return $code === '23503';
80+
}
81+
82+
/**
83+
* Checks whether the native database error represents a NOT NULL constraint violation.
84+
*/
85+
protected function isNotNullConstraintViolation(int|string $code, string $message): bool
86+
{
87+
return $code === '23502';
88+
}
89+
90+
/**
91+
* Checks whether the native database error represents a CHECK constraint violation.
92+
*/
93+
protected function isCheckConstraintViolation(int|string $code, string $message): bool
94+
{
95+
return $code === '23514';
96+
}
97+
98+
/**
99+
* Checks whether the native database error represents a constraint violation.
100+
*/
101+
protected function isConstraintViolation(int|string $code, string $message): bool
102+
{
103+
return is_string($code) && str_starts_with($code, '23');
104+
}
105+
74106
/**
75107
* Checks whether the native database code represents a retryable transaction failure.
76108
*/

system/Database/SQLSRV/Connection.php

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,10 @@ class Connection extends BaseConnection
9494
*/
9595
protected function isUniqueConstraintViolation(int|string $code, string $message): bool
9696
{
97-
$code = (string) $code;
97+
$vendorCode = $this->getVendorErrorCode($code);
9898

99-
if (str_contains($code, '/')) {
100-
[$sqlstate, $vendorCode] = explode('/', $code, 2);
101-
102-
if ($sqlstate === '23000' && in_array((int) $vendorCode, [2627, 2601], true)) {
103-
return true;
104-
}
99+
if ($vendorCode !== null && in_array($vendorCode, [2627, 2601], true)) {
100+
return $this->hasSQLState($code, '23000');
105101
}
106102

107103
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
@@ -121,20 +117,55 @@ protected function isUniqueConstraintViolation(int|string $code, string $message
121117
return false;
122118
}
123119

120+
/**
121+
* Checks whether the native database error represents a NOT NULL constraint violation.
122+
*/
123+
protected function isNotNullConstraintViolation(int|string $code, string $message): bool
124+
{
125+
return $this->getVendorErrorCode($code) === 515
126+
&& $this->hasSQLState($code, '23000');
127+
}
128+
129+
/**
130+
* Checks whether the native database error represents a constraint violation.
131+
*/
132+
protected function isConstraintViolation(int|string $code, string $message): bool
133+
{
134+
return $this->getSQLState($code) === '23000'
135+
|| ($this->getVendorErrorCode($code) === 547 && $this->hasSQLState($code, '23000'));
136+
}
137+
124138
/**
125139
* Checks whether the native database code represents a retryable transaction failure.
126140
*/
127141
protected function isRetryableTransactionErrorCode(int|string $code): bool
142+
{
143+
$vendorCode = $this->getVendorErrorCode($code);
144+
145+
return $vendorCode !== null && in_array($vendorCode, [1205, 3960], true);
146+
}
147+
148+
private function getVendorErrorCode(int|string $code): ?int
128149
{
129150
$vendorCode = (string) (is_string($code) && str_contains($code, '/')
130151
? substr($code, strrpos($code, '/') + 1)
131152
: $code);
132153

133-
if (preg_match('/^\d+$/', $vendorCode) !== 1) {
134-
return false;
135-
}
154+
return preg_match('/^\d+$/', $vendorCode) === 1 ? (int) $vendorCode : null;
155+
}
156+
157+
private function getSQLState(int|string $code): string
158+
{
159+
return is_string($code) && str_contains($code, '/')
160+
? substr($code, 0, strpos($code, '/'))
161+
: (string) $code;
162+
}
136163

137-
return in_array((int) $vendorCode, [1205, 3960], true);
164+
private function hasSQLState(int|string $code, string $sqlstate): bool
165+
{
166+
return ! is_string($code)
167+
|| ! str_contains($code, '/')
168+
|| $this->getSQLState($code) === $sqlstate;
138169
}
139170

140171
/**

0 commit comments

Comments
 (0)