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

codeigniter4 / CodeIgniter4 / 22495985083

27 Feb 2026 05:08PM UTC coverage: 86.622% (+0.03%) from 86.594%
22495985083

push

github

web-flow
refactor: remove deprecations in `Database` (#9986)

7 of 9 new or added lines in 2 files covered. (77.78%)

283 existing lines in 13 files now uncovered.

22513 of 25990 relevant lines covered (86.62%)

218.55 hits per line

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

91.84
/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 $busyTimeout;
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
     * @return void
72
     */
73
    public function initialize()
74
    {
75
        parent::initialize();
778✔
76

77
        if ($this->foreignKeys) {
778✔
78
            $this->enableForeignKeyChecks();
777✔
79
        }
80

81
        if (is_int($this->busyTimeout)) {
778✔
82
            $this->connID->busyTimeout($this->busyTimeout);
777✔
83
        }
84

85
        if (is_int($this->synchronous)) {
778✔
86
            if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
777✔
87
                throw new InvalidArgumentException('Invalid synchronous value.');
1✔
88
            }
89
            $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
776✔
90
        }
91
    }
92

93
    /**
94
     * Connect to the database.
95
     *
96
     * @return SQLite3
97
     *
98
     * @throws DatabaseException
99
     */
100
    public function connect(bool $persistent = false)
101
    {
102
        if ($persistent && $this->DBDebug) {
34✔
UNCOV
103
            throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
×
104
        }
105

106
        try {
107
            if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
34✔
108
                $this->database = WRITEPATH . $this->database;
30✔
109
            }
110

111
            $sqlite = ($this->password === null || $this->password === '')
34✔
112
                ? new SQLite3($this->database)
34✔
UNCOV
113
                : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
×
114

115
            $sqlite->enableExceptions(true);
34✔
116

117
            return $sqlite;
34✔
118
        } catch (Exception $e) {
1✔
119
            throw new DatabaseException('SQLite3 error: ' . $e->getMessage(), $e->getCode(), $e);
1✔
120
        }
121
    }
122

123
    /**
124
     * Close the database connection.
125
     *
126
     * @return void
127
     */
128
    protected function _close()
129
    {
130
        $this->connID->close();
3✔
131
    }
132

133
    /**
134
     * Select a specific database table to use.
135
     */
136
    public function setDatabase(string $databaseName): bool
137
    {
UNCOV
138
        return false;
×
139
    }
140

141
    /**
142
     * Returns a string containing the version of the database being used.
143
     */
144
    public function getVersion(): string
145
    {
146
        if (isset($this->dataCache['version'])) {
790✔
147
            return $this->dataCache['version'];
789✔
148
        }
149

150
        $version = SQLite3::version();
58✔
151

152
        return $this->dataCache['version'] = $version['versionString'];
58✔
153
    }
154

155
    /**
156
     * Execute the query
157
     *
158
     * @return false|SQLite3Result
159
     */
160
    protected function execute(string $sql)
161
    {
162
        try {
163
            return $this->isWriteType($sql)
787✔
164
                ? $this->connID->exec($sql)
748✔
165
                : $this->connID->query($sql);
787✔
166
        } catch (Exception $e) {
33✔
167
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
33✔
168
                'message' => $e->getMessage(),
33✔
169
                'exFile'  => clean_path($e->getFile()),
33✔
170
                'exLine'  => $e->getLine(),
33✔
171
                'trace'   => render_backtrace($e->getTrace()),
33✔
172
            ]);
33✔
173

174
            $error     = $this->error();
33✔
175
            $exception = $this->isUniqueConstraintViolation($e->getMessage())
33✔
176
                ? new UniqueConstraintViolationException($e->getMessage(), $error['code'], $e)
19✔
177
                : new DatabaseException($e->getMessage(), $error['code'], $e);
14✔
178

179
            if ($this->DBDebug) {
33✔
180
                throw $exception;
14✔
181
            }
182

183
            $this->lastException = $exception;
19✔
184
        }
185

186
        return false;
19✔
187
    }
188

189
    private function isUniqueConstraintViolation(string $message): bool
190
    {
191
        // SQLite3 reports unique violations in two formats depending on version:
192
        // Modern:  "UNIQUE constraint failed: table.column"
193
        // Legacy:  "column X is not unique"
194
        return str_contains($message, 'UNIQUE constraint failed')
33✔
195
            || str_contains($message, 'is not unique');
33✔
196
    }
197

198
    /**
199
     * Returns the total number of rows affected by this query.
200
     */
201
    public function affectedRows(): int
202
    {
203
        return $this->connID->changes();
51✔
204
    }
205

206
    /**
207
     * Platform-dependant string escape
208
     */
209
    protected function _escapeString(string $str): string
210
    {
211
        if (! $this->connID instanceof SQLite3) {
754✔
212
            $this->initialize();
1✔
213
        }
214

215
        return $this->connID->escapeString($str);
754✔
216
    }
217

218
    /**
219
     * Generates the SQL for listing tables in a platform-dependent manner.
220
     *
221
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
222
     */
223
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
224
    {
225
        if ((string) $tableName !== '') {
701✔
226
            return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
690✔
227
                   . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
690✔
228
                   . ' AND "NAME" LIKE ' . $this->escape($tableName);
690✔
229
        }
230

231
        return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
55✔
232
               . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
55✔
233
               . (($prefixLimit && $this->DBPrefix !== '')
55✔
UNCOV
234
                    ? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
×
235
                    : '');
55✔
236
    }
237

238
    /**
239
     * Generates a platform-specific query string so that the column names can be fetched.
240
     *
241
     * @param string|TableName $table
242
     */
243
    protected function _listColumns($table = ''): string
244
    {
245
        if ($table instanceof TableName) {
12✔
246
            $tableName = $this->escapeIdentifier($table);
2✔
247
        } else {
248
            $tableName = $this->protectIdentifiers($table, true, null, false);
10✔
249
        }
250

251
        return 'PRAGMA TABLE_INFO(' . $tableName . ')';
12✔
252
    }
253

254
    /**
255
     * @param string|TableName $tableName
256
     *
257
     * @return false|list<string>
258
     *
259
     * @throws DatabaseException
260
     */
261
    public function getFieldNames($tableName)
262
    {
263
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
16✔
264

265
        // Is there a cached result?
266
        if (isset($this->dataCache['field_names'][$table])) {
16✔
267
            return $this->dataCache['field_names'][$table];
9✔
268
        }
269

270
        if (! $this->connID instanceof SQLite3) {
12✔
UNCOV
271
            $this->initialize();
×
272
        }
273

274
        $sql = $this->_listColumns($tableName);
12✔
275

276
        $query                                  = $this->query($sql);
12✔
277
        $this->dataCache['field_names'][$table] = [];
12✔
278

279
        foreach ($query->getResultArray() as $row) {
12✔
280
            // Do we know from where to get the column's name?
281
            if (! isset($key)) {
12✔
282
                if (isset($row['column_name'])) {
12✔
UNCOV
283
                    $key = 'column_name';
×
284
                } elseif (isset($row['COLUMN_NAME'])) {
12✔
UNCOV
285
                    $key = 'COLUMN_NAME';
×
286
                } elseif (isset($row['name'])) {
12✔
287
                    $key = 'name';
12✔
288
                } else {
289
                    // We have no other choice but to just get the first element's key.
UNCOV
290
                    $key = key($row);
×
291
                }
292
            }
293

294
            $this->dataCache['field_names'][$table][] = $row[$key];
12✔
295
        }
296

297
        return $this->dataCache['field_names'][$table];
12✔
298
    }
299

300
    /**
301
     * Returns an array of objects with field data
302
     *
303
     * @return list<stdClass>
304
     *
305
     * @throws DatabaseException
306
     */
307
    protected function _fieldData(string $table): array
308
    {
309
        if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
41✔
UNCOV
310
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
311
        }
312

313
        $query = $query->getResultObject();
41✔
314

315
        if (empty($query)) {
41✔
UNCOV
316
            return [];
×
317
        }
318

319
        $retVal = [];
41✔
320

321
        for ($i = 0, $c = count($query); $i < $c; $i++) {
41✔
322
            $retVal[$i] = new stdClass();
41✔
323

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

335
        return $retVal;
41✔
336
    }
337

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

363
        if (($query = $this->query($sql)) === false) {
51✔
UNCOV
364
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
365
        }
366
        $query = $query->getResultObject();
51✔
367

368
        $tempVal = [];
51✔
369

370
        foreach ($query as $row) {
51✔
371
            if ($row->indextype === 'PRIMARY') {
34✔
372
                $tempVal['PRIMARY']['indextype']               = $row->indextype;
31✔
373
                $tempVal['PRIMARY']['indexname']               = $row->indexname;
31✔
374
                $tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
31✔
375
            } else {
376
                $tempVal[$row->indexname]['indextype']               = $row->indextype;
25✔
377
                $tempVal[$row->indexname]['indexname']               = $row->indexname;
25✔
378
                $tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
25✔
379
            }
380
        }
381

382
        $retVal = [];
51✔
383

384
        foreach ($tempVal as $val) {
51✔
385
            $obj                = new stdClass();
34✔
386
            $obj->name          = $val['indexname'];
34✔
387
            $obj->fields        = array_values($val['fields']);
34✔
388
            $obj->type          = $val['indextype'];
34✔
389
            $retVal[$obj->name] = $obj;
34✔
390
        }
391

392
        return $retVal;
51✔
393
    }
394

395
    /**
396
     * Returns an array of objects with Foreign key data
397
     *
398
     * @return array<string, stdClass>
399
     */
400
    protected function _foreignKeyData(string $table): array
401
    {
402
        if (! $this->supportsForeignKeys()) {
36✔
UNCOV
403
            return [];
×
404
        }
405

406
        $query   = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
36✔
407
        $indexes = [];
36✔
408

409
        foreach ($query as $row) {
36✔
410
            $indexes[$row->id]['constraint_name']       = null;
11✔
411
            $indexes[$row->id]['table_name']            = $table;
11✔
412
            $indexes[$row->id]['foreign_table_name']    = $row->table;
11✔
413
            $indexes[$row->id]['column_name'][]         = $row->from;
11✔
414
            $indexes[$row->id]['foreign_column_name'][] = $row->to;
11✔
415
            $indexes[$row->id]['on_delete']             = $row->on_delete;
11✔
416
            $indexes[$row->id]['on_update']             = $row->on_update;
11✔
417
            $indexes[$row->id]['match']                 = $row->match;
11✔
418
        }
419

420
        return $this->foreignKeyDataToObjects($indexes);
36✔
421
    }
422

423
    /**
424
     * Returns platform-specific SQL to disable foreign key checks.
425
     *
426
     * @return string
427
     */
428
    protected function _disableForeignKeyChecks()
429
    {
430
        return 'PRAGMA foreign_keys = OFF';
704✔
431
    }
432

433
    /**
434
     * Returns platform-specific SQL to enable foreign key checks.
435
     *
436
     * @return string
437
     */
438
    protected function _enableForeignKeyChecks()
439
    {
440
        return 'PRAGMA foreign_keys = ON';
787✔
441
    }
442

443
    /**
444
     * Returns the last error code and message.
445
     * Must return this format: ['code' => string|int, 'message' => string]
446
     * intval(code) === 0 means "no error".
447
     *
448
     * @return array<string, int|string>
449
     */
450
    public function error(): array
451
    {
452
        return [
35✔
453
            'code'    => $this->connID->lastErrorCode(),
35✔
454
            'message' => $this->connID->lastErrorMsg(),
35✔
455
        ];
35✔
456
    }
457

458
    /**
459
     * Insert ID
460
     */
461
    public function insertID(): int
462
    {
463
        return $this->connID->lastInsertRowID();
83✔
464
    }
465

466
    /**
467
     * Begin Transaction
468
     */
469
    protected function _transBegin(): bool
470
    {
471
        return $this->connID->exec('BEGIN TRANSACTION');
52✔
472
    }
473

474
    /**
475
     * Commit Transaction
476
     */
477
    protected function _transCommit(): bool
478
    {
479
        return $this->connID->exec('END TRANSACTION');
38✔
480
    }
481

482
    /**
483
     * Rollback Transaction
484
     */
485
    protected function _transRollback(): bool
486
    {
487
        return $this->connID->exec('ROLLBACK');
19✔
488
    }
489

490
    /**
491
     * Checks to see if the current install supports Foreign Keys
492
     * and has them enabled.
493
     */
494
    public function supportsForeignKeys(): bool
495
    {
496
        $result = $this->simpleQuery('PRAGMA foreign_keys');
36✔
497

498
        return (bool) $result;
36✔
499
    }
500
}
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