Skip to content

Commit fb4c345

Browse files
committed
fix(database): classify prepared query exceptions
- Reuse semantic database exception classification for prepared query failures - Store typed exceptions for prepared queries when DBDebug is disabled - Document prepared query exception behavior Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
1 parent df9f137 commit fb4c345

14 files changed

Lines changed: 264 additions & 47 deletions

File tree

system/Database/BaseConnection.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,16 @@ public function getLastException(): ?DatabaseException
21402140
return $this->lastException;
21412141
}
21422142

2143+
/**
2144+
* Sets the exception for the last failed database operation.
2145+
*
2146+
* @internal This method is for internal database component use only.
2147+
*/
2148+
public function setLastException(?DatabaseException $exception): void
2149+
{
2150+
$this->lastException = $exception;
2151+
}
2152+
21432153
/**
21442154
* Checks whether the native database error represents a unique constraint violation.
21452155
*/
@@ -2158,8 +2168,10 @@ protected function isRetryableTransactionErrorCode(int|string $code): bool
21582168

21592169
/**
21602170
* Creates the appropriate database exception for a native database error.
2171+
*
2172+
* @internal This method is for internal database component use only.
21612173
*/
2162-
protected function createDatabaseException(
2174+
public function createDatabaseException(
21632175
string $message,
21642176
int|string $code = 0,
21652177
?Throwable $previous = null,

system/Database/BasePreparedQuery.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use CodeIgniter\Events\Events;
1919
use CodeIgniter\Exceptions\BadMethodCallException;
2020
use ErrorException;
21+
use Throwable;
2122

2223
/**
2324
* @template TConnection
@@ -49,6 +50,11 @@ abstract class BasePreparedQuery implements PreparedQueryInterface
4950
*/
5051
protected $errorString;
5152

53+
/**
54+
* The typed exception for the last failed prepared query, if any.
55+
*/
56+
protected ?DatabaseException $databaseException = null;
57+
5258
/**
5359
* Holds the prepared query object
5460
* that is cloned during execute.
@@ -121,8 +127,10 @@ public function execute(...$data)
121127

122128
try {
123129
$exception = null;
124-
$result = $this->_execute($data);
125-
} catch (ArgumentCountError|ErrorException $exception) {
130+
$this->db->setLastException(null);
131+
$this->databaseException = null;
132+
$result = $this->_execute($data);
133+
} catch (ArgumentCountError|DatabaseException|ErrorException $exception) {
126134
$result = false;
127135
}
128136

@@ -136,6 +144,8 @@ public function execute(...$data)
136144
// This will trigger a rollback if transactions are being used
137145
$this->db->handleTransStatus();
138146

147+
$databaseException = $this->createDatabaseException($exception);
148+
139149
if ($this->db->DBDebug) {
140150
// We call this function in order to roll-back queries
141151
// if transactions are enabled. If we don't call this here
@@ -154,8 +164,8 @@ public function execute(...$data)
154164
// Let others do something with this query.
155165
Events::trigger('DBQuery', $query);
156166

157-
if ($exception !== null) {
158-
throw new DatabaseException($exception->getMessage(), $exception->getCode(), $exception);
167+
if ($databaseException instanceof DatabaseException) {
168+
throw $databaseException;
159169
}
160170

161171
return false;
@@ -164,6 +174,8 @@ public function execute(...$data)
164174
// Let others do something with this query.
165175
Events::trigger('DBQuery', $query);
166176

177+
$this->db->setLastException($databaseException);
178+
167179
return false;
168180
}
169181

@@ -196,6 +208,34 @@ abstract public function _execute(array $data): bool;
196208
*/
197209
abstract public function _getResult();
198210

211+
/**
212+
* Creates the database exception for a failed prepared query.
213+
*/
214+
private function createDatabaseException(?Throwable $previous): ?DatabaseException
215+
{
216+
if ($previous instanceof DatabaseException) {
217+
return $previous;
218+
}
219+
220+
if ($this->databaseException instanceof DatabaseException) {
221+
return $this->databaseException;
222+
}
223+
224+
if ($previous instanceof Throwable) {
225+
return $this->db->createDatabaseException(
226+
$previous->getMessage(),
227+
$previous->getCode(),
228+
$previous,
229+
);
230+
}
231+
232+
if ($this->errorString === null || $this->errorString === '') {
233+
return null;
234+
}
235+
236+
return $this->db->createDatabaseException($this->errorString, $this->errorCode);
237+
}
238+
199239
/**
200240
* Explicitly closes the prepared statement.
201241
*

system/Database/MySQLi/PreparedQuery.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace CodeIgniter\Database\MySQLi;
1515

1616
use CodeIgniter\Database\BasePreparedQuery;
17-
use CodeIgniter\Database\Exceptions\DatabaseException;
1817
use CodeIgniter\Exceptions\BadMethodCallException;
1918
use mysqli;
2019
use mysqli_result;
@@ -49,7 +48,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery
4948
$this->errorString = $this->db->mysqli->error;
5049

5150
if ($this->db->DBDebug) {
52-
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
51+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
5352
}
5453
}
5554

@@ -93,14 +92,29 @@ public function _execute(array $data): bool
9392
}
9493

9594
try {
96-
return $this->statement->execute();
95+
$result = $this->statement->execute();
9796
} catch (mysqli_sql_exception $e) {
97+
$this->errorCode = $e->getCode();
98+
$this->errorString = $e->getMessage();
99+
$this->databaseException = $this->db->createDatabaseException($this->errorString, $this->errorCode, $e);
100+
98101
if ($this->db->DBDebug) {
99-
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
102+
throw $this->databaseException;
100103
}
101104

102105
return false;
103106
}
107+
108+
if ($result === false) {
109+
$this->errorCode = $this->statement->errno;
110+
$this->errorString = $this->statement->error;
111+
112+
if ($this->db->DBDebug) {
113+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
114+
}
115+
}
116+
117+
return $result;
104118
}
105119

106120
/**

system/Database/OCI8/PreparedQuery.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace CodeIgniter\Database\OCI8;
1515

1616
use CodeIgniter\Database\BasePreparedQuery;
17-
use CodeIgniter\Database\Exceptions\DatabaseException;
1817
use CodeIgniter\Exceptions\BadMethodCallException;
1918
use OCILob;
2019

@@ -55,7 +54,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery
5554
$this->errorString = $error['message'] ?? '';
5655

5756
if ($this->db->DBDebug) {
58-
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
57+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
5958
}
6059
}
6160

@@ -92,6 +91,16 @@ public function _execute(array $data): bool
9291
$binaryData->free();
9392
}
9493

94+
if ($result === false) {
95+
$error = oci_error($this->statement);
96+
$this->errorCode = $error['code'] ?? 0;
97+
$this->errorString = $error['message'] ?? '';
98+
99+
if ($this->db->DBDebug) {
100+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
101+
}
102+
}
103+
95104
if ($result && $this->lastInsertTableName !== '') {
96105
$this->db->lastInsertedTableName = $this->lastInsertTableName;
97106
}

system/Database/Postgre/PreparedQuery.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace CodeIgniter\Database\Postgre;
1515

1616
use CodeIgniter\Database\BasePreparedQuery;
17-
use CodeIgniter\Database\Exceptions\DatabaseException;
1817
use CodeIgniter\Exceptions\BadMethodCallException;
1918
use Exception;
2019
use PgSql\Connection as PgSqlConnection;
@@ -70,7 +69,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery
7069
$this->errorString = pg_last_error($this->db->connID);
7170

7271
if ($this->db->DBDebug) {
73-
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
72+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
7473
}
7574
}
7675

@@ -95,6 +94,28 @@ public function _execute(array $data): bool
9594

9695
$this->result = pg_execute($this->db->connID, $this->name, $data);
9796

97+
if ($this->result instanceof PgSqlResult && pg_result_status($this->result) === PGSQL_FATAL_ERROR) {
98+
$sqlstate = (string) pg_result_error_field($this->result, PGSQL_DIAG_SQLSTATE);
99+
$this->errorCode = 0;
100+
$this->errorString = (string) pg_result_error($this->result);
101+
$this->databaseException = $this->db->createDatabaseException($this->errorString, $sqlstate);
102+
103+
if ($this->db->DBDebug) {
104+
throw $this->databaseException;
105+
}
106+
107+
return false;
108+
}
109+
110+
if ($this->result === false) {
111+
$this->errorCode = 0;
112+
$this->errorString = pg_last_error($this->db->connID);
113+
114+
if ($this->db->DBDebug) {
115+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
116+
}
117+
}
118+
98119
return (bool) $this->result;
99120
}
100121

system/Database/SQLSRV/Connection.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ class Connection extends BaseConnection
9494
*/
9595
protected function isUniqueConstraintViolation(int|string $code, string $message): bool
9696
{
97+
$code = (string) $code;
98+
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+
}
105+
}
106+
97107
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
98108
if (! is_array($errors)) {
99109
return false;

system/Database/SQLSRV/PreparedQuery.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ public function _prepare(string $sql, array $options = []): PreparedQuery
6565
$this->statement = sqlsrv_prepare($this->db->connID, $sql, $parameters);
6666

6767
if (! $this->statement) {
68+
$info = $this->db->error();
69+
$this->databaseException = $this->db->createDatabaseException($this->db->getAllErrorMessages(), $info['code']);
70+
6871
if ($this->db->DBDebug) {
69-
throw new DatabaseException($this->db->getAllErrorMessages());
72+
throw $this->databaseException;
7073
}
7174

72-
$info = $this->db->error();
73-
$this->errorCode = $info['code'];
75+
$this->errorCode = is_int($info['code']) ? $info['code'] : 0;
7476
$this->errorString = $info['message'];
7577
}
7678

@@ -93,8 +95,16 @@ public function _execute(array $data): bool
9395

9496
$result = sqlsrv_execute($this->statement);
9597

96-
if ($result === false && $this->db->DBDebug) {
97-
throw new DatabaseException($this->db->getAllErrorMessages());
98+
if ($result === false) {
99+
$error = $this->db->error();
100+
101+
$this->errorCode = is_int($error['code']) ? $error['code'] : 0;
102+
$this->errorString = $this->db->getAllErrorMessages();
103+
$this->databaseException = $this->db->createDatabaseException($this->errorString, $error['code']);
104+
105+
if ($this->db->DBDebug) {
106+
throw $this->databaseException;
107+
}
98108
}
99109

100110
return $result;

system/Database/SQLite3/PreparedQuery.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace CodeIgniter\Database\SQLite3;
1515

1616
use CodeIgniter\Database\BasePreparedQuery;
17-
use CodeIgniter\Database\Exceptions\DatabaseException;
1817
use CodeIgniter\Exceptions\BadMethodCallException;
1918
use Exception;
2019
use SQLite3;
@@ -52,7 +51,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery
5251
$this->errorString = $this->db->connID->lastErrorMsg();
5352

5453
if ($this->db->DBDebug) {
55-
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
54+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
5655
}
5756
}
5857

@@ -88,13 +87,27 @@ public function _execute(array $data): bool
8887
try {
8988
$this->result = $this->statement->execute();
9089
} catch (Exception $e) {
90+
$error = $this->db->error();
91+
$this->errorCode = $error['code'];
92+
$this->errorString = $e->getMessage();
93+
$this->databaseException = $this->db->createDatabaseException($this->errorString, $this->errorCode, $e);
94+
9195
if ($this->db->DBDebug) {
92-
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
96+
throw $this->databaseException;
9397
}
9498

9599
return false;
96100
}
97101

102+
if ($this->result === false) {
103+
$this->errorCode = $this->db->connID->lastErrorCode();
104+
$this->errorString = $this->db->connID->lastErrorMsg();
105+
106+
if ($this->db->DBDebug) {
107+
throw $this->db->createDatabaseException($this->errorString, $this->errorCode);
108+
}
109+
}
110+
98111
return $this->result !== false;
99112
}
100113

tests/_support/Mock/MockPreparedQuery.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Tests\Support\Mock;
1515

1616
use CodeIgniter\Database\BasePreparedQuery;
17+
use Throwable;
1718

1819
/**
1920
* @internal
@@ -22,7 +23,8 @@
2223
*/
2324
final class MockPreparedQuery extends BasePreparedQuery
2425
{
25-
public string $preparedSql = '';
26+
public string $preparedSql = '';
27+
public ?Throwable $thrownException = null;
2628

2729
/**
2830
* @param array<string, mixed> $options
@@ -39,6 +41,10 @@ public function _prepare(string $sql, array $options = []): self
3941
*/
4042
public function _execute(array $data): bool
4143
{
44+
if ($this->thrownException instanceof Throwable) {
45+
throw $this->thrownException;
46+
}
47+
4248
return true;
4349
}
4450

0 commit comments

Comments
 (0)