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

codeigniter4 / CodeIgniter4 / 25449128090

06 May 2026 04:54PM UTC coverage: 88.285% (-0.002%) from 88.287%
25449128090

Pull #10162

github

web-flow
Merge 3fe354db9 into 3f912495d
Pull Request #10162: feat: classify retryable transaction exceptions

11 of 13 new or added lines in 6 files covered. (84.62%)

213 existing lines in 6 files now uncovered.

23535 of 26658 relevant lines covered (88.28%)

218.2 hits per line

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

91.89
/system/Database/SQLite3/Connection.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\SQLite3;
15

16
use CodeIgniter\Database\BaseConnection;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
19
use CodeIgniter\Database\TableName;
20
use CodeIgniter\Exceptions\InvalidArgumentException;
21
use Exception;
22
use SQLite3;
23
use SQLite3Result;
24
use stdClass;
25

26
/**
27
 * Connection for SQLite3
28
 *
29
 * @extends BaseConnection<SQLite3, SQLite3Result>
30
 */
31
class Connection extends BaseConnection
32
{
33
    /**
34
     * Database driver
35
     *
36
     * @var string
37
     */
38
    public $DBDriver = 'SQLite3';
39

40
    /**
41
     * Identifier escape character
42
     *
43
     * @var string
44
     */
45
    public $escapeChar = '`';
46

47
    /**
48
     * @var bool Enable Foreign Key constraint or not
49
     */
50
    protected $foreignKeys = false;
51

52
    /**
53
     * The milliseconds to sleep
54
     *
55
     * @var int|null milliseconds
56
     *
57
     * @see https://www.php.net/manual/en/sqlite3.busytimeout
58
     */
59
    protected ?int $busyTimeout = null;
60

61
    /**
62
     * The setting of the "synchronous" flag
63
     *
64
     * @var int<0, 3>|null flag
65
     *
66
     * @see https://www.sqlite.org/pragma.html#pragma_synchronous
67
     */
68
    protected ?int $synchronous = null;
69

70
    /**
71
     * Checks whether the native database code represents a retryable transaction failure.
72
     */
73
    protected function isRetryableTransactionErrorCode(int|string $code): bool
74
    {
75
        return $code === 5;
4✔
76
    }
77

78
    /**
79
     * @return void
80
     */
81
    public function initialize()
82
    {
83
        parent::initialize();
818✔
84

85
        if ($this->foreignKeys) {
818✔
86
            $this->enableForeignKeyChecks();
817✔
87
        }
88

89
        if (is_int($this->busyTimeout)) {
818✔
90
            $this->connID->busyTimeout($this->busyTimeout);
817✔
91
        }
92

93
        if (is_int($this->synchronous)) {
818✔
94
            if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
817✔
95
                throw new InvalidArgumentException('Invalid synchronous value.');
1✔
96
            }
97
            $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
816✔
98
        }
99
    }
100

101
    /**
102
     * Connect to the database.
103
     *
104
     * @return SQLite3
105
     *
106
     * @throws DatabaseException
107
     */
108
    public function connect(bool $persistent = false)
109
    {
110
        if ($persistent && $this->DBDebug) {
61✔
UNCOV
111
            throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
×
112
        }
113

114
        try {
115
            if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
61✔
116
                $this->database = WRITEPATH . $this->database;
57✔
117
            }
118

119
            $sqlite = ($this->password === null || $this->password === '')
61✔
120
                ? new SQLite3($this->database)
61✔
UNCOV
121
                : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
×
122

123
            $sqlite->enableExceptions(true);
61✔
124

125
            return $sqlite;
61✔
126
        } catch (Exception $e) {
1✔
127
            throw new DatabaseException('SQLite3 error: ' . $e->getMessage(), $e->getCode(), $e);
1✔
128
        }
129
    }
130

131
    /**
132
     * Close the database connection.
133
     *
134
     * @return void
135
     */
136
    protected function _close()
137
    {
138
        $this->connID->close();
3✔
139
    }
140

141
    /**
142
     * Select a specific database table to use.
143
     */
144
    public function setDatabase(string $databaseName): bool
145
    {
UNCOV
146
        return false;
×
147
    }
148

149
    /**
150
     * Returns a string containing the version of the database being used.
151
     */
152
    public function getVersion(): string
153
    {
154
        if (isset($this->dataCache['version'])) {
830✔
155
            return $this->dataCache['version'];
829✔
156
        }
157

158
        $version = SQLite3::version();
85✔
159

160
        return $this->dataCache['version'] = $version['versionString'];
85✔
161
    }
162

163
    /**
164
     * Execute the query
165
     *
166
     * @return false|SQLite3Result
167
     */
168
    protected function execute(string $sql)
169
    {
170
        try {
171
            return $this->isWriteType($sql)
827✔
172
                ? $this->connID->exec($sql)
788✔
173
                : $this->connID->query($sql);
827✔
174
        } catch (Exception $e) {
37✔
175
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
37✔
176
                'message' => $e->getMessage(),
37✔
177
                'exFile'  => clean_path($e->getFile()),
37✔
178
                'exLine'  => $e->getLine(),
37✔
179
                'trace'   => render_backtrace($e->getTrace()),
37✔
180
            ]);
37✔
181

182
            $error     = $this->error();
37✔
183
            $exception = $this->isUniqueConstraintViolation($e->getMessage())
37✔
184
                ? new UniqueConstraintViolationException($e->getMessage(), $error['code'], $e)
23✔
185
                : new DatabaseException($e->getMessage(), $error['code'], $e);
14✔
186

187
            if ($this->DBDebug) {
37✔
188
                throw $exception;
15✔
189
            }
190

191
            $this->lastException = $exception;
22✔
192
        }
193

194
        return false;
22✔
195
    }
196

197
    private function isUniqueConstraintViolation(string $message): bool
198
    {
199
        // SQLite3 reports unique violations in two formats depending on version:
200
        // Modern:  "UNIQUE constraint failed: table.column"
201
        // Legacy:  "column X is not unique"
202
        return str_contains($message, 'UNIQUE constraint failed')
37✔
203
            || str_contains($message, 'is not unique');
37✔
204
    }
205

206
    /**
207
     * Returns the total number of rows affected by this query.
208
     */
209
    public function affectedRows(): int
210
    {
211
        return $this->connID->changes();
51✔
212
    }
213

214
    /**
215
     * Platform-dependant string escape
216
     */
217
    protected function _escapeString(string $str): string
218
    {
219
        if (! $this->connID instanceof SQLite3) {
794✔
220
            $this->initialize();
1✔
221
        }
222

223
        return $this->connID->escapeString($str);
794✔
224
    }
225

226
    /**
227
     * Generates the SQL for listing tables in a platform-dependent manner.
228
     *
229
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
230
     */
231
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
232
    {
233
        if ((string) $tableName !== '') {
741✔
234
            return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
730✔
235
                   . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
730✔
236
                   . ' AND "NAME" LIKE ' . $this->escape($tableName);
730✔
237
        }
238

239
        return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
82✔
240
               . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
82✔
241
               . (($prefixLimit && $this->DBPrefix !== '')
82✔
242
                    ? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
×
243
                    : '');
82✔
244
    }
245

246
    /**
247
     * Generates a platform-specific query string so that the column names can be fetched.
248
     *
249
     * @param string|TableName $table
250
     */
251
    protected function _listColumns($table = ''): string
252
    {
253
        if ($table instanceof TableName) {
12✔
254
            $tableName = $this->escapeIdentifier($table);
2✔
255
        } else {
256
            $tableName = $this->protectIdentifiers($table, true, null, false);
10✔
257
        }
258

259
        return 'PRAGMA TABLE_INFO(' . $tableName . ')';
12✔
260
    }
261

262
    /**
263
     * @param string|TableName $tableName
264
     *
265
     * @return false|list<string>
266
     *
267
     * @throws DatabaseException
268
     */
269
    public function getFieldNames($tableName)
270
    {
271
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
16✔
272

273
        // Is there a cached result?
274
        if (isset($this->dataCache['field_names'][$table])) {
16✔
275
            return $this->dataCache['field_names'][$table];
9✔
276
        }
277

278
        if (! $this->connID instanceof SQLite3) {
12✔
279
            $this->initialize();
×
280
        }
281

282
        $sql = $this->_listColumns($tableName);
12✔
283

284
        $query                                  = $this->query($sql);
12✔
285
        $this->dataCache['field_names'][$table] = [];
12✔
286

287
        foreach ($query->getResultArray() as $row) {
12✔
288
            // Do we know from where to get the column's name?
289
            if (! isset($key)) {
12✔
290
                if (isset($row['column_name'])) {
12✔
291
                    $key = 'column_name';
×
292
                } elseif (isset($row['COLUMN_NAME'])) {
12✔
293
                    $key = 'COLUMN_NAME';
×
294
                } elseif (isset($row['name'])) {
12✔
295
                    $key = 'name';
12✔
296
                } else {
297
                    // We have no other choice but to just get the first element's key.
298
                    $key = key($row);
×
299
                }
300
            }
301

302
            $this->dataCache['field_names'][$table][] = $row[$key];
12✔
303
        }
304

305
        return $this->dataCache['field_names'][$table];
12✔
306
    }
307

308
    /**
309
     * Returns an array of objects with field data
310
     *
311
     * @return list<stdClass>
312
     *
313
     * @throws DatabaseException
314
     */
315
    protected function _fieldData(string $table): array
316
    {
317
        if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
41✔
318
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
319
        }
320

321
        $query = $query->getResultObject();
41✔
322

323
        if (empty($query)) {
41✔
324
            return [];
×
325
        }
326

327
        $retVal = [];
41✔
328

329
        for ($i = 0, $c = count($query); $i < $c; $i++) {
41✔
330
            $retVal[$i] = new stdClass();
41✔
331

332
            $retVal[$i]->name       = $query[$i]->name;
41✔
333
            $retVal[$i]->type       = $query[$i]->type;
41✔
334
            $retVal[$i]->max_length = null;
41✔
335
            $retVal[$i]->nullable   = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
41✔
336
            $retVal[$i]->default    = $query[$i]->dflt_value;
41✔
337
            // "pk" (either zero for columns that are not part of the primary key,
338
            // or the 1-based index of the column within the primary key).
339
            // https://www.sqlite.org/pragma.html#pragma_table_info
340
            $retVal[$i]->primary_key = ($query[$i]->pk === 0) ? 0 : 1;
41✔
341
        }
342

343
        return $retVal;
41✔
344
    }
345

346
    /**
347
     * Returns an array of objects with index data
348
     *
349
     * @return array<string, stdClass>
350
     *
351
     * @throws DatabaseException
352
     */
353
    protected function _indexData(string $table): array
354
    {
355
        $sql = "SELECT 'PRIMARY' as indexname, l.name as fieldname, 'PRIMARY' as indextype
51✔
356
                FROM pragma_table_info(" . $this->escape(strtolower($table)) . ") as l
51✔
357
                WHERE l.pk <> 0
358
                UNION ALL
359
                SELECT sqlite_master.name as indexname, ii.name as fieldname,
360
                CASE
361
                WHEN ti.pk <> 0 AND sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'PRIMARY'
362
                WHEN sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'UNIQUE'
363
                WHEN sqlite_master.sql LIKE '% UNIQUE %' THEN 'UNIQUE'
364
                ELSE 'INDEX'
365
                END as indextype
366
                FROM sqlite_master
367
                INNER JOIN pragma_index_xinfo(sqlite_master.name) ii ON ii.name IS NOT NULL
368
                LEFT JOIN pragma_table_info(" . $this->escape(strtolower($table)) . ") ti ON ti.name = ii.name
51✔
369
                WHERE sqlite_master.type='index' AND sqlite_master.tbl_name = " . $this->escape(strtolower($table)) . ' COLLATE NOCASE';
51✔
370

371
        if (($query = $this->query($sql)) === false) {
51✔
372
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
373
        }
374
        $query = $query->getResultObject();
51✔
375

376
        $tempVal = [];
51✔
377

378
        foreach ($query as $row) {
51✔
379
            if ($row->indextype === 'PRIMARY') {
34✔
380
                $tempVal['PRIMARY']['indextype']               = $row->indextype;
31✔
381
                $tempVal['PRIMARY']['indexname']               = $row->indexname;
31✔
382
                $tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
31✔
383
            } else {
384
                $tempVal[$row->indexname]['indextype']               = $row->indextype;
25✔
385
                $tempVal[$row->indexname]['indexname']               = $row->indexname;
25✔
386
                $tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
25✔
387
            }
388
        }
389

390
        $retVal = [];
51✔
391

392
        foreach ($tempVal as $val) {
51✔
393
            $obj                = new stdClass();
34✔
394
            $obj->name          = $val['indexname'];
34✔
395
            $obj->fields        = array_values($val['fields']);
34✔
396
            $obj->type          = $val['indextype'];
34✔
397
            $retVal[$obj->name] = $obj;
34✔
398
        }
399

400
        return $retVal;
51✔
401
    }
402

403
    /**
404
     * Returns an array of objects with Foreign key data
405
     *
406
     * @return array<string, stdClass>
407
     */
408
    protected function _foreignKeyData(string $table): array
409
    {
410
        if (! $this->supportsForeignKeys()) {
36✔
411
            return [];
×
412
        }
413

414
        $query   = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
36✔
415
        $indexes = [];
36✔
416

417
        foreach ($query as $row) {
36✔
418
            $indexes[$row->id]['constraint_name']       = null;
11✔
419
            $indexes[$row->id]['table_name']            = $table;
11✔
420
            $indexes[$row->id]['foreign_table_name']    = $row->table;
11✔
421
            $indexes[$row->id]['column_name'][]         = $row->from;
11✔
422
            $indexes[$row->id]['foreign_column_name'][] = $row->to;
11✔
423
            $indexes[$row->id]['on_delete']             = $row->on_delete;
11✔
424
            $indexes[$row->id]['on_update']             = $row->on_update;
11✔
425
            $indexes[$row->id]['match']                 = $row->match;
11✔
426
        }
427

428
        return $this->foreignKeyDataToObjects($indexes);
36✔
429
    }
430

431
    /**
432
     * Returns platform-specific SQL to disable foreign key checks.
433
     *
434
     * @return string
435
     */
436
    protected function _disableForeignKeyChecks()
437
    {
438
        return 'PRAGMA foreign_keys = OFF';
744✔
439
    }
440

441
    /**
442
     * Returns platform-specific SQL to enable foreign key checks.
443
     *
444
     * @return string
445
     */
446
    protected function _enableForeignKeyChecks()
447
    {
448
        return 'PRAGMA foreign_keys = ON';
827✔
449
    }
450

451
    /**
452
     * Returns the last error code and message.
453
     * Must return this format: ['code' => string|int, 'message' => string]
454
     * intval(code) === 0 means "no error".
455
     *
456
     * @return array<string, int|string>
457
     */
458
    public function error(): array
459
    {
460
        return [
39✔
461
            'code'    => $this->connID->lastErrorCode(),
39✔
462
            'message' => $this->connID->lastErrorMsg(),
39✔
463
        ];
39✔
464
    }
465

466
    /**
467
     * Insert ID
468
     */
469
    public function insertID(): int
470
    {
471
        return $this->connID->lastInsertRowID();
88✔
472
    }
473

474
    /**
475
     * Begin Transaction
476
     */
477
    protected function _transBegin(): bool
478
    {
479
        return $this->connID->exec('BEGIN TRANSACTION');
77✔
480
    }
481

482
    /**
483
     * Commit Transaction
484
     */
485
    protected function _transCommit(): bool
486
    {
487
        return $this->connID->exec('END TRANSACTION');
49✔
488
    }
489

490
    /**
491
     * Rollback Transaction
492
     */
493
    protected function _transRollback(): bool
494
    {
495
        return $this->connID->exec('ROLLBACK');
33✔
496
    }
497

498
    /**
499
     * Checks to see if the current install supports Foreign Keys
500
     * and has them enabled.
501
     */
502
    public function supportsForeignKeys(): bool
503
    {
504
        $result = $this->simpleQuery('PRAGMA foreign_keys');
36✔
505

506
        return (bool) $result;
36✔
507
    }
508
}
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