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

codeigniter4 / CodeIgniter4 / 25902734269

15 May 2026 05:51AM UTC coverage: 88.459% (+0.2%) from 88.299%
25902734269

Pull #10159

github

web-flow
Merge f0573f3e0 into 170b89a6e
Pull Request #10159: feat: Add support for callable TTLs in cache handlers

6 of 10 new or added lines in 3 files covered. (60.0%)

446 existing lines in 24 files now uncovered.

24114 of 27260 relevant lines covered (88.46%)

219.07 hits per line

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

91.78
/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\TableName;
19
use CodeIgniter\Exceptions\InvalidArgumentException;
20
use Exception;
21
use SQLite3;
22
use SQLite3Result;
23
use stdClass;
24

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

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

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

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

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

69
    /**
70
     * Checks whether the native database error represents a unique constraint violation.
71
     */
72
    protected function isUniqueConstraintViolation(int|string $code, string $message): bool
73
    {
74
        // SQLite3 reports unique violations in two formats depending on version:
75
        // Modern:  "UNIQUE constraint failed: table.column"
76
        // Legacy:  "column X is not unique"
77
        return str_contains($message, 'UNIQUE constraint failed')
48✔
78
            || str_contains($message, 'is not unique');
48✔
79
    }
80

81
    /**
82
     * Checks whether the native database code represents a retryable transaction failure.
83
     */
84
    protected function isRetryableTransactionErrorCode(int|string $code): bool
85
    {
86
        return $code === 5;
19✔
87
    }
88

89
    /**
90
     * @return void
91
     */
92
    public function initialize()
93
    {
94
        parent::initialize();
836✔
95

96
        if ($this->foreignKeys) {
836✔
97
            $this->enableForeignKeyChecks();
835✔
98
        }
99

100
        if (is_int($this->busyTimeout)) {
836✔
101
            $this->connID->busyTimeout($this->busyTimeout);
835✔
102
        }
103

104
        if (is_int($this->synchronous)) {
836✔
105
            if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
835✔
106
                throw new InvalidArgumentException('Invalid synchronous value.');
1✔
107
            }
108
            $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
834✔
109
        }
110
    }
111

112
    /**
113
     * Connect to the database.
114
     *
115
     * @return SQLite3
116
     *
117
     * @throws DatabaseException
118
     */
119
    public function connect(bool $persistent = false)
120
    {
121
        if ($persistent && $this->DBDebug) {
61✔
UNCOV
122
            throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
×
123
        }
124

125
        try {
126
            if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
61✔
127
                $this->database = WRITEPATH . $this->database;
57✔
128
            }
129

130
            $sqlite = ($this->password === null || $this->password === '')
61✔
131
                ? new SQLite3($this->database)
61✔
UNCOV
132
                : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
×
133

134
            $sqlite->enableExceptions(true);
61✔
135

136
            return $sqlite;
61✔
137
        } catch (Exception $e) {
1✔
138
            throw new DatabaseException('SQLite3 error: ' . $e->getMessage(), $e->getCode(), $e);
1✔
139
        }
140
    }
141

142
    /**
143
     * Close the database connection.
144
     *
145
     * @return void
146
     */
147
    protected function _close()
148
    {
149
        $this->connID->close();
3✔
150
    }
151

152
    /**
153
     * Select a specific database table to use.
154
     */
155
    public function setDatabase(string $databaseName): bool
156
    {
UNCOV
157
        return false;
×
158
    }
159

160
    /**
161
     * Returns a string containing the version of the database being used.
162
     */
163
    public function getVersion(): string
164
    {
165
        if (isset($this->dataCache['version'])) {
848✔
166
            return $this->dataCache['version'];
847✔
167
        }
168

169
        $version = SQLite3::version();
85✔
170

171
        return $this->dataCache['version'] = $version['versionString'];
85✔
172
    }
173

174
    /**
175
     * Execute the query
176
     *
177
     * @return false|SQLite3Result
178
     */
179
    protected function execute(string $sql)
180
    {
181
        try {
182
            return $this->isWriteType($sql)
845✔
183
                ? $this->connID->exec($sql)
806✔
184
                : $this->connID->query($sql);
845✔
185
        } catch (Exception $e) {
37✔
186
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
37✔
187
                'message' => $e->getMessage(),
37✔
188
                'exFile'  => clean_path($e->getFile()),
37✔
189
                'exLine'  => $e->getLine(),
37✔
190
                'trace'   => render_backtrace($e->getTrace()),
37✔
191
            ]);
37✔
192

193
            $error     = $this->error();
37✔
194
            $exception = $this->createDatabaseException($e->getMessage(), $error['code'], $e);
37✔
195

196
            if ($this->DBDebug) {
37✔
197
                throw $exception;
15✔
198
            }
199

200
            $this->lastException = $exception;
22✔
201
        }
202

203
        return false;
22✔
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) {
812✔
220
            $this->initialize();
1✔
221
        }
222

223
        return $this->connID->escapeString($str);
812✔
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 !== '') {
759✔
234
            return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
748✔
235
                   . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
748✔
236
                   . ' AND "NAME" LIKE ' . $this->escape($tableName);
748✔
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✔
UNCOV
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✔
UNCOV
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✔
UNCOV
291
                    $key = 'column_name';
×
292
                } elseif (isset($row['COLUMN_NAME'])) {
12✔
UNCOV
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.
UNCOV
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✔
UNCOV
318
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
319
        }
320

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

323
        if (empty($query)) {
41✔
UNCOV
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✔
UNCOV
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✔
UNCOV
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';
762✔
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';
845✔
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{code: int|string|null, message: string|null}
457
     */
458
    public function error(): array
459
    {
460
        return [
44✔
461
            'code'    => $this->connID->lastErrorCode(),
44✔
462
            'message' => $this->connID->lastErrorMsg(),
44✔
463
        ];
44✔
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