• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 25637286072

10 May 2026 07:09PM UTC coverage: 88.366% (-0.05%) from 88.418%
25637286072

Pull #10182

github

web-flow
Merge 4a0b49c91 into df9f13771
Pull Request #10182: fix(database): classify prepared query exceptions

77 of 108 new or added lines in 9 files covered. (71.3%)

1 existing line in 1 file now uncovered.

23911 of 27059 relevant lines covered (88.37%)

218.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.79
/system/Database/BasePreparedQuery.php
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;
15

16
use ArgumentCountError;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Events\Events;
19
use CodeIgniter\Exceptions\BadMethodCallException;
20
use ErrorException;
21
use Throwable;
22

23
/**
24
 * @template TConnection
25
 * @template TStatement
26
 * @template TResult
27
 *
28
 * @implements PreparedQueryInterface<TConnection, TStatement, TResult>
29
 */
30
abstract class BasePreparedQuery implements PreparedQueryInterface
31
{
32
    /**
33
     * The prepared statement itself.
34
     *
35
     * @var TStatement|null
36
     */
37
    protected $statement;
38

39
    /**
40
     * The error code, if any.
41
     *
42
     * @var int
43
     */
44
    protected $errorCode;
45

46
    /**
47
     * The error message, if any.
48
     *
49
     * @var string
50
     */
51
    protected $errorString;
52

53
    /**
54
     * The typed exception for the last failed prepared query, if any.
55
     */
56
    protected ?DatabaseException $databaseException = null;
57

58
    /**
59
     * Holds the prepared query object
60
     * that is cloned during execute.
61
     *
62
     * @var Query
63
     */
64
    protected $query;
65

66
    /**
67
     * A reference to the db connection to use.
68
     *
69
     * @var BaseConnection<TConnection, TResult>
70
     */
71
    protected $db;
72

73
    public function __construct(BaseConnection $db)
74
    {
75
        $this->db = $db;
23✔
76
    }
77

78
    /**
79
     * Prepares the query against the database, and saves the connection
80
     * info necessary to execute the query later.
81
     *
82
     * NOTE: This version is based on SQL code. Child classes should
83
     * override this method.
84
     *
85
     * @return $this
86
     */
87
    public function prepare(string $sql, array $options = [], string $queryClass = Query::class)
88
    {
89
        // We only support positional placeholders (?), so convert
90
        // named placeholders (:name or :name:) while leaving dialect
91
        // syntax like PostgreSQL casts (::type) untouched.
92
        $sql = preg_replace('/(?<!:):([a-zA-Z_]\w*):?(?!:)/', '?', $sql);
23✔
93

94
        /** @var Query $query */
95
        $query = new $queryClass($this->db);
23✔
96

97
        $query->setQuery($sql);
23✔
98

99
        if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
23✔
100
            $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
×
101
        }
102

103
        $this->query = $query;
23✔
104

105
        return $this->_prepare($query->getOriginalQuery(), $options);
23✔
106
    }
107

108
    /**
109
     * The database-dependent portion of the prepare statement.
110
     *
111
     * @return $this
112
     */
113
    abstract public function _prepare(string $sql, array $options = []);
114

115
    /**
116
     * Takes a new set of data and runs it against the currently
117
     * prepared query. Upon success, will return a Results object.
118
     *
119
     * @return bool|ResultInterface<TConnection, TResult>
120
     *
121
     * @throws DatabaseException
122
     */
123
    public function execute(...$data)
124
    {
125
        // Execute the Query.
126
        $startTime = microtime(true);
17✔
127

128
        try {
129
            $exception = null;
17✔
130
            $this->db->setLastException(null);
17✔
131
            $this->databaseException = null;
17✔
132
            $result                  = $this->_execute($data);
17✔
133
        } catch (ArgumentCountError|DatabaseException|ErrorException $exception) {
6✔
134
            $result = false;
5✔
135
        }
136

137
        // Update our query object
138
        $query = clone $this->query;
16✔
139
        $query->setBinds($data);
16✔
140

141
        if ($result === false) {
16✔
142
            $query->setDuration($startTime, $startTime);
8✔
143

144
            // This will trigger a rollback if transactions are being used
145
            $this->db->handleTransStatus();
8✔
146

147
            $databaseException = $this->createDatabaseException($exception);
8✔
148

149
            if ($this->db->DBDebug) {
8✔
150
                // We call this function in order to roll-back queries
151
                // if transactions are enabled. If we don't call this here
152
                // the error message will trigger an exit, causing the
153
                // transactions to remain in limbo.
154
                while ($this->db->transDepth !== 0) {
4✔
155
                    $transDepth = $this->db->transDepth;
×
156
                    $this->db->transComplete();
×
157

158
                    if ($transDepth === $this->db->transDepth) {
×
159
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
160
                        break;
×
161
                    }
162
                }
163

164
                // Let others do something with this query.
165
                Events::trigger('DBQuery', $query);
4✔
166

167
                if ($databaseException instanceof DatabaseException) {
4✔
168
                    throw $databaseException;
4✔
169
                }
170

171
                return false;
×
172
            }
173

174
            // Let others do something with this query.
175
            Events::trigger('DBQuery', $query);
4✔
176

177
            $this->db->setLastException($databaseException);
4✔
178

179
            return false;
4✔
180
        }
181

182
        $query->setDuration($startTime);
12✔
183

184
        // Let others do something with this query
185
        Events::trigger('DBQuery', $query);
12✔
186

187
        if ($this->db->isWriteType((string) $query)) {
12✔
188
            return true;
8✔
189
        }
190

191
        // Return a result object
192
        $resultClass = str_replace('PreparedQuery', 'Result', static::class);
4✔
193

194
        $resultID = $this->_getResult();
4✔
195

196
        return new $resultClass($this->db->connID, $resultID);
4✔
197
    }
198

199
    /**
200
     * The database dependant version of the execute method.
201
     */
202
    abstract public function _execute(array $data): bool;
203

204
    /**
205
     * Returns the result object for the prepared query.
206
     *
207
     * @return object|resource|null
208
     */
209
    abstract public function _getResult();
210

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) {
8✔
217
            return $previous;
3✔
218
        }
219

220
        if ($this->databaseException instanceof DatabaseException) {
6✔
221
            return $this->databaseException;
3✔
222
        }
223

224
        if ($previous instanceof Throwable) {
3✔
225
            return $this->db->createDatabaseException(
3✔
226
                $previous->getMessage(),
3✔
227
                $previous->getCode(),
3✔
228
                $previous,
3✔
229
            );
3✔
230
        }
231

NEW
232
        if ($this->errorString === null || $this->errorString === '') {
×
NEW
233
            return null;
×
234
        }
235

NEW
236
        return $this->db->createDatabaseException($this->errorString, $this->errorCode);
×
237
    }
238

239
    /**
240
     * Explicitly closes the prepared statement.
241
     *
242
     * @throws BadMethodCallException
243
     */
244
    public function close(): bool
245
    {
246
        if (! isset($this->statement)) {
16✔
247
            throw new BadMethodCallException('Cannot call close on a non-existing prepared statement.');
2✔
248
        }
249

250
        try {
251
            return $this->_close();
16✔
252
        } finally {
253
            $this->statement = null;
16✔
254
        }
255
    }
256

257
    /**
258
     * The database-dependent version of the close method.
259
     */
260
    abstract protected function _close(): bool;
261

262
    /**
263
     * Returns the SQL that has been prepared.
264
     */
265
    public function getQueryString(): string
266
    {
267
        if (! $this->query instanceof QueryInterface) {
16✔
268
            throw new BadMethodCallException('Cannot call getQueryString on a prepared query until after the query has been prepared.');
×
269
        }
270

271
        return $this->query->getQuery();
16✔
272
    }
273

274
    /**
275
     * A helper to determine if any error exists.
276
     */
277
    public function hasError(): bool
278
    {
279
        return ! empty($this->errorString);
×
280
    }
281

282
    /**
283
     * Returns the error code created while executing this statement.
284
     */
285
    public function getErrorCode(): int
286
    {
287
        return $this->errorCode;
×
288
    }
289

290
    /**
291
     * Returns the error message created while executing this statement.
292
     */
293
    public function getErrorMessage(): string
294
    {
295
        return $this->errorString;
×
296
    }
297

298
    /**
299
     * Whether the input contain binary data.
300
     */
301
    protected function isBinary(string $input): bool
302
    {
303
        return mb_detect_encoding($input, 'UTF-8', true) === false;
13✔
304
    }
305
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc