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

codeigniter4 / CodeIgniter4 / 18389505594

09 Oct 2025 09:21PM UTC coverage: 84.39% (+0.03%) from 84.362%
18389505594

Pull #9751

github

web-flow
Merge 3d707799b into d945236b2
Pull Request #9751: refactor(app): Standardize subdomain detection logic

16 of 16 new or added lines in 3 files covered. (100.0%)

43 existing lines in 6 files now uncovered.

21236 of 25164 relevant lines covered (84.39%)

195.78 hits per line

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

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

76
        if ($this->foreignKeys) {
681✔
77
            $this->enableForeignKeyChecks();
680✔
78
        }
79

80
        if (is_int($this->busyTimeout)) {
681✔
81
            $this->connID->busyTimeout($this->busyTimeout);
680✔
82
        }
83

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

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

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

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

114
            $sqlite->enableExceptions(true);
31✔
115

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

122
    /**
123
     * Keep or establish the connection if no queries have been sent for
124
     * a length of time exceeding the server's idle timeout.
125
     *
126
     * @return void
127
     */
128
    public function reconnect()
129
    {
130
        $this->close();
×
131
        $this->initialize();
×
132
    }
133

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

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

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

161
        $version = SQLite3::version();
56✔
162

163
        return $this->dataCache['version'] = $version['versionString'];
56✔
164
    }
165

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

185
            if ($this->DBDebug) {
31✔
186
                throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
13✔
187
            }
188
        }
189

190
        return false;
18✔
191
    }
192

193
    /**
194
     * Returns the total number of rows affected by this query.
195
     */
196
    public function affectedRows(): int
197
    {
198
        return $this->connID->changes();
50✔
199
    }
200

201
    /**
202
     * Platform-dependant string escape
203
     */
204
    protected function _escapeString(string $str): string
205
    {
206
        if (! $this->connID instanceof SQLite3) {
661✔
207
            $this->initialize();
1✔
208
        }
209

210
        return $this->connID->escapeString($str);
661✔
211
    }
212

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

226
        return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
53✔
227
               . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
53✔
228
               . (($prefixLimit && $this->DBPrefix !== '')
53✔
229
                    ? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
×
230
                    : '');
53✔
231
    }
232

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

246
        return 'PRAGMA TABLE_INFO(' . $tableName . ')';
12✔
247
    }
248

249
    /**
250
     * @param string|TableName $tableName
251
     *
252
     * @return false|list<string>
253
     *
254
     * @throws DatabaseException
255
     */
256
    public function getFieldNames($tableName)
257
    {
258
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
16✔
259

260
        // Is there a cached result?
261
        if (isset($this->dataCache['field_names'][$table])) {
16✔
262
            return $this->dataCache['field_names'][$table];
9✔
263
        }
264

265
        if (! $this->connID instanceof SQLite3) {
12✔
266
            $this->initialize();
×
267
        }
268

269
        $sql = $this->_listColumns($tableName);
12✔
270

271
        $query                                  = $this->query($sql);
12✔
272
        $this->dataCache['field_names'][$table] = [];
12✔
273

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

289
            $this->dataCache['field_names'][$table][] = $row[$key];
12✔
290
        }
291

292
        return $this->dataCache['field_names'][$table];
12✔
293
    }
294

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

308
        $query = $query->getResultObject();
41✔
309

310
        if (empty($query)) {
41✔
311
            return [];
×
312
        }
313

314
        $retVal = [];
41✔
315

316
        for ($i = 0, $c = count($query); $i < $c; $i++) {
41✔
317
            $retVal[$i] = new stdClass();
41✔
318

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

330
        return $retVal;
41✔
331
    }
332

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

358
        if (($query = $this->query($sql)) === false) {
51✔
359
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
360
        }
361
        $query = $query->getResultObject();
51✔
362

363
        $tempVal = [];
51✔
364

365
        foreach ($query as $row) {
51✔
366
            if ($row->indextype === 'PRIMARY') {
34✔
367
                $tempVal['PRIMARY']['indextype']               = $row->indextype;
31✔
368
                $tempVal['PRIMARY']['indexname']               = $row->indexname;
31✔
369
                $tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
31✔
370
            } else {
371
                $tempVal[$row->indexname]['indextype']               = $row->indextype;
25✔
372
                $tempVal[$row->indexname]['indexname']               = $row->indexname;
25✔
373
                $tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
25✔
374
            }
375
        }
376

377
        $retVal = [];
51✔
378

379
        foreach ($tempVal as $val) {
51✔
380
            $obj                = new stdClass();
34✔
381
            $obj->name          = $val['indexname'];
34✔
382
            $obj->fields        = array_values($val['fields']);
34✔
383
            $obj->type          = $val['indextype'];
34✔
384
            $retVal[$obj->name] = $obj;
34✔
385
        }
386

387
        return $retVal;
51✔
388
    }
389

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

401
        $query   = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
36✔
402
        $indexes = [];
36✔
403

404
        foreach ($query as $row) {
36✔
405
            $indexes[$row->id]['constraint_name']       = null;
11✔
406
            $indexes[$row->id]['table_name']            = $table;
11✔
407
            $indexes[$row->id]['foreign_table_name']    = $row->table;
11✔
408
            $indexes[$row->id]['column_name'][]         = $row->from;
11✔
409
            $indexes[$row->id]['foreign_column_name'][] = $row->to;
11✔
410
            $indexes[$row->id]['on_delete']             = $row->on_delete;
11✔
411
            $indexes[$row->id]['on_update']             = $row->on_update;
11✔
412
            $indexes[$row->id]['match']                 = $row->match;
11✔
413
        }
414

415
        return $this->foreignKeyDataToObjects($indexes);
36✔
416
    }
417

418
    /**
419
     * Returns platform-specific SQL to disable foreign key checks.
420
     *
421
     * @return string
422
     */
423
    protected function _disableForeignKeyChecks()
424
    {
425
        return 'PRAGMA foreign_keys = OFF';
611✔
426
    }
427

428
    /**
429
     * Returns platform-specific SQL to enable foreign key checks.
430
     *
431
     * @return string
432
     */
433
    protected function _enableForeignKeyChecks()
434
    {
435
        return 'PRAGMA foreign_keys = ON';
690✔
436
    }
437

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

453
    /**
454
     * Insert ID
455
     */
456
    public function insertID(): int
457
    {
458
        return $this->connID->lastInsertRowID();
83✔
459
    }
460

461
    /**
462
     * Begin Transaction
463
     */
464
    protected function _transBegin(): bool
465
    {
466
        return $this->connID->exec('BEGIN TRANSACTION');
51✔
467
    }
468

469
    /**
470
     * Commit Transaction
471
     */
472
    protected function _transCommit(): bool
473
    {
474
        return $this->connID->exec('END TRANSACTION');
38✔
475
    }
476

477
    /**
478
     * Rollback Transaction
479
     */
480
    protected function _transRollback(): bool
481
    {
482
        return $this->connID->exec('ROLLBACK');
18✔
483
    }
484

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

493
        return (bool) $result;
36✔
494
    }
495
}
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