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

codeigniter4 / CodeIgniter4 / 28995226889

09 Jul 2026 04:59AM UTC coverage: 89.607% (+0.02%) from 89.588%
28995226889

Pull #10395

github

web-flow
Merge d47c78aa8 into 0d4f36d76
Pull Request #10395: feat: add StreamResponse and SSE response factories

31 of 41 new or added lines in 3 files covered. (75.61%)

223 existing lines in 6 files now uncovered.

25330 of 28268 relevant lines covered (89.61%)

230.92 hits per line

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

92.0
/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')
62✔
78
            || str_contains($message, 'is not unique');
62✔
79
    }
80

81
    /**
82
     * Checks whether the native database error represents a foreign key constraint violation.
83
     */
84
    protected function isForeignKeyConstraintViolation(int|string $code, string $message): bool
85
    {
86
        return str_contains($message, 'FOREIGN KEY constraint failed');
27✔
87
    }
88

89
    /**
90
     * Checks whether the native database error represents a NOT NULL constraint violation.
91
     */
92
    protected function isNotNullConstraintViolation(int|string $code, string $message): bool
93
    {
94
        return str_contains($message, 'NOT NULL constraint failed');
25✔
95
    }
96

97
    /**
98
     * Checks whether the native database error represents a CHECK constraint violation.
99
     */
100
    protected function isCheckConstraintViolation(int|string $code, string $message): bool
101
    {
102
        return str_contains($message, 'CHECK constraint failed');
19✔
103
    }
104

105
    /**
106
     * Checks whether the native database error represents a constraint violation.
107
     */
108
    protected function isConstraintViolation(int|string $code, string $message): bool
109
    {
110
        return $code === 19;
18✔
111
    }
112

113
    /**
114
     * Checks whether the native database code represents a retryable transaction failure.
115
     */
116
    protected function isRetryableTransactionErrorCode(int|string $code): bool
117
    {
118
        return $code === 5;
16✔
119
    }
120

121
    /**
122
     * @return void
123
     */
124
    public function initialize()
125
    {
126
        parent::initialize();
921✔
127

128
        if ($this->foreignKeys) {
921✔
129
            $this->enableForeignKeyChecks();
920✔
130
        }
131

132
        if (is_int($this->busyTimeout)) {
921✔
133
            $this->connID->busyTimeout($this->busyTimeout);
920✔
134
        }
135

136
        if (is_int($this->synchronous)) {
921✔
137
            if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
920✔
138
                throw new InvalidArgumentException('Invalid synchronous value.');
1✔
139
            }
140
            $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
919✔
141
        }
142
    }
143

144
    /**
145
     * Connect to the database.
146
     *
147
     * @return SQLite3
148
     *
149
     * @throws DatabaseException
150
     */
151
    public function connect(bool $persistent = false)
152
    {
153
        if ($persistent && $this->DBDebug) {
77✔
UNCOV
154
            throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
×
155
        }
156

157
        try {
158
            if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
77✔
159
                $this->database = WRITEPATH . $this->database;
72✔
160
            }
161

162
            $sqlite = ($this->password === null || $this->password === '')
77✔
163
                ? new SQLite3($this->database)
77✔
UNCOV
164
                : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
×
165

166
            $sqlite->enableExceptions(true);
77✔
167

168
            return $sqlite;
77✔
169
        } catch (Exception $e) {
1✔
170
            throw new DatabaseException('SQLite3 error: ' . $e->getMessage(), $e->getCode(), $e);
1✔
171
        }
172
    }
173

174
    /**
175
     * Close the database connection.
176
     *
177
     * @return void
178
     */
179
    protected function _close()
180
    {
181
        $this->connID->close();
4✔
182
    }
183

184
    /**
185
     * Select a specific database table to use.
186
     */
187
    public function setDatabase(string $databaseName): bool
188
    {
UNCOV
189
        return false;
×
190
    }
191

192
    /**
193
     * Returns a string containing the version of the database being used.
194
     */
195
    public function getVersion(): string
196
    {
197
        if (isset($this->dataCache['version'])) {
926✔
198
            return $this->dataCache['version'];
925✔
199
        }
200

201
        $version = SQLite3::version();
114✔
202

203
        return $this->dataCache['version'] = $version['versionString'];
114✔
204
    }
205

206
    /**
207
     * Execute the query
208
     *
209
     * @return false|SQLite3Result
210
     */
211
    protected function execute(string $sql)
212
    {
213
        try {
214
            return $this->isWriteType($sql)
923✔
215
                ? $this->connID->exec($sql)
889✔
216
                : $this->connID->query($sql);
923✔
217
        } catch (Exception $e) {
47✔
218
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
47✔
219
                'message' => $e->getMessage(),
47✔
220
                'exFile'  => clean_path($e->getFile()),
47✔
221
                'exLine'  => $e->getLine(),
47✔
222
                'trace'   => render_backtrace($e->getTrace()),
47✔
223
            ]);
47✔
224

225
            $error     = $this->error();
47✔
226
            $exception = $this->createDatabaseException($e->getMessage(), $error['code'], $e);
47✔
227

228
            if ($this->DBDebug) {
47✔
229
                throw $exception;
21✔
230
            }
231

232
            $this->lastException = $exception;
26✔
233
        }
234

235
        return false;
26✔
236
    }
237

238
    /**
239
     * Returns the total number of rows affected by this query.
240
     */
241
    public function affectedRows(): int
242
    {
243
        return $this->connID->changes();
55✔
244
    }
245

246
    /**
247
     * Platform-dependant string escape
248
     */
249
    protected function _escapeString(string $str): string
250
    {
251
        if (! $this->connID instanceof SQLite3) {
897✔
252
            $this->initialize();
1✔
253
        }
254

255
        return $this->connID->escapeString($str);
897✔
256
    }
257

258
    /**
259
     * Generates the SQL for listing tables in a platform-dependent manner.
260
     *
261
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
262
     */
263
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
264
    {
265
        if ((string) $tableName !== '') {
843✔
266
            return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
832✔
267
                   . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
832✔
268
                   . ' AND "NAME" LIKE ' . $this->escape($tableName);
832✔
269
        }
270

271
        return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
109✔
272
               . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
109✔
273
               . (($prefixLimit && $this->DBPrefix !== '')
109✔
UNCOV
274
                    ? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
×
275
                    : '');
109✔
276
    }
277

278
    /**
279
     * Generates a platform-specific query string so that the column names can be fetched.
280
     *
281
     * @param string|TableName $table
282
     */
283
    protected function _listColumns($table = ''): string
284
    {
285
        if ($table instanceof TableName) {
17✔
286
            $tableName = $this->escapeIdentifier($table);
7✔
287
        } else {
288
            $tableName = $this->protectIdentifiers($table, true, null, false);
10✔
289
        }
290

291
        return 'PRAGMA TABLE_INFO(' . $tableName . ')';
17✔
292
    }
293

294
    /**
295
     * @param string|TableName $tableName
296
     *
297
     * @return false|list<string>
298
     *
299
     * @throws DatabaseException
300
     */
301
    public function getFieldNames($tableName)
302
    {
303
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
17✔
304

305
        // Is there a cached result?
306
        if (isset($this->dataCache['field_names'][$table])) {
17✔
307
            return $this->dataCache['field_names'][$table];
5✔
308
        }
309

310
        if (! $this->connID instanceof SQLite3) {
17✔
UNCOV
311
            $this->initialize();
×
312
        }
313

314
        $sql = $this->_listColumns($tableName);
17✔
315

316
        $query                                  = $this->query($sql);
17✔
317
        $this->dataCache['field_names'][$table] = [];
17✔
318

319
        foreach ($query->getResultArray() as $row) {
17✔
320
            // Do we know from where to get the column's name?
321
            if (! isset($key)) {
17✔
322
                if (isset($row['column_name'])) {
17✔
UNCOV
323
                    $key = 'column_name';
×
324
                } elseif (isset($row['COLUMN_NAME'])) {
17✔
UNCOV
325
                    $key = 'COLUMN_NAME';
×
326
                } elseif (isset($row['name'])) {
17✔
327
                    $key = 'name';
17✔
328
                } else {
329
                    // We have no other choice but to just get the first element's key.
UNCOV
330
                    $key = key($row);
×
331
                }
332
            }
333

334
            $this->dataCache['field_names'][$table][] = $row[$key];
17✔
335
        }
336

337
        return $this->dataCache['field_names'][$table];
17✔
338
    }
339

340
    /**
341
     * Returns an array of objects with field data
342
     *
343
     * @return list<stdClass>
344
     *
345
     * @throws DatabaseException
346
     */
347
    protected function _fieldData(string $table): array
348
    {
349
        if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
43✔
UNCOV
350
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
351
        }
352

353
        $query = $query->getResultObject();
43✔
354

355
        if (empty($query)) {
43✔
UNCOV
356
            return [];
×
357
        }
358

359
        $retVal = [];
43✔
360

361
        for ($i = 0, $c = count($query); $i < $c; $i++) {
43✔
362
            $retVal[$i] = new stdClass();
43✔
363

364
            $retVal[$i]->name       = $query[$i]->name;
43✔
365
            $retVal[$i]->type       = $query[$i]->type;
43✔
366
            $retVal[$i]->max_length = null;
43✔
367
            $retVal[$i]->nullable   = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
43✔
368
            $retVal[$i]->default    = $query[$i]->dflt_value;
43✔
369
            // "pk" (either zero for columns that are not part of the primary key,
370
            // or the 1-based index of the column within the primary key).
371
            // https://www.sqlite.org/pragma.html#pragma_table_info
372
            $retVal[$i]->primary_key = ($query[$i]->pk === 0) ? 0 : 1;
43✔
373
        }
374

375
        return $retVal;
43✔
376
    }
377

378
    /**
379
     * Returns an array of objects with index data
380
     *
381
     * @return array<string, stdClass>
382
     *
383
     * @throws DatabaseException
384
     */
385
    protected function _indexData(string $table): array
386
    {
387
        $sql = "SELECT 'PRIMARY' as indexname, l.name as fieldname, 'PRIMARY' as indextype
51✔
388
                FROM pragma_table_info(" . $this->escape(strtolower($table)) . ") as l
51✔
389
                WHERE l.pk <> 0
390
                UNION ALL
391
                SELECT sqlite_master.name as indexname, ii.name as fieldname,
392
                CASE
393
                WHEN ti.pk <> 0 AND sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'PRIMARY'
394
                WHEN sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'UNIQUE'
395
                WHEN sqlite_master.sql LIKE '% UNIQUE %' THEN 'UNIQUE'
396
                ELSE 'INDEX'
397
                END as indextype
398
                FROM sqlite_master
399
                INNER JOIN pragma_index_xinfo(sqlite_master.name) ii ON ii.name IS NOT NULL
400
                LEFT JOIN pragma_table_info(" . $this->escape(strtolower($table)) . ") ti ON ti.name = ii.name
51✔
401
                WHERE sqlite_master.type='index' AND sqlite_master.tbl_name = " . $this->escape(strtolower($table)) . ' COLLATE NOCASE';
51✔
402

403
        if (($query = $this->query($sql)) === false) {
51✔
UNCOV
404
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
405
        }
406
        $query = $query->getResultObject();
51✔
407

408
        $tempVal = [];
51✔
409

410
        foreach ($query as $row) {
51✔
411
            if ($row->indextype === 'PRIMARY') {
34✔
412
                $tempVal['PRIMARY']['indextype']               = $row->indextype;
31✔
413
                $tempVal['PRIMARY']['indexname']               = $row->indexname;
31✔
414
                $tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
31✔
415
            } else {
416
                $tempVal[$row->indexname]['indextype']               = $row->indextype;
25✔
417
                $tempVal[$row->indexname]['indexname']               = $row->indexname;
25✔
418
                $tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
25✔
419
            }
420
        }
421

422
        $retVal = [];
51✔
423

424
        foreach ($tempVal as $val) {
51✔
425
            $obj                = new stdClass();
34✔
426
            $obj->name          = $val['indexname'];
34✔
427
            $obj->fields        = array_values($val['fields']);
34✔
428
            $obj->type          = $val['indextype'];
34✔
429
            $retVal[$obj->name] = $obj;
34✔
430
        }
431

432
        return $retVal;
51✔
433
    }
434

435
    /**
436
     * Returns an array of objects with Foreign key data
437
     *
438
     * @return array<string, stdClass>
439
     */
440
    protected function _foreignKeyData(string $table): array
441
    {
442
        if (! $this->supportsForeignKeys()) {
36✔
UNCOV
443
            return [];
×
444
        }
445

446
        $query   = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
36✔
447
        $indexes = [];
36✔
448

449
        foreach ($query as $row) {
36✔
450
            $indexes[$row->id]['constraint_name']       = null;
11✔
451
            $indexes[$row->id]['table_name']            = $table;
11✔
452
            $indexes[$row->id]['foreign_table_name']    = $row->table;
11✔
453
            $indexes[$row->id]['column_name'][]         = $row->from;
11✔
454
            $indexes[$row->id]['foreign_column_name'][] = $row->to;
11✔
455
            $indexes[$row->id]['on_delete']             = $row->on_delete;
11✔
456
            $indexes[$row->id]['on_update']             = $row->on_update;
11✔
457
            $indexes[$row->id]['match']                 = $row->match;
11✔
458
        }
459

460
        return $this->foreignKeyDataToObjects($indexes);
36✔
461
    }
462

463
    /**
464
     * Returns platform-specific SQL to disable foreign key checks.
465
     *
466
     * @return string
467
     */
468
    protected function _disableForeignKeyChecks()
469
    {
470
        return 'PRAGMA foreign_keys = OFF';
846✔
471
    }
472

473
    /**
474
     * Returns platform-specific SQL to enable foreign key checks.
475
     *
476
     * @return string
477
     */
478
    protected function _enableForeignKeyChecks()
479
    {
480
        return 'PRAGMA foreign_keys = ON';
923✔
481
    }
482

483
    /**
484
     * Returns the last error code and message.
485
     * Must return this format: ['code' => string|int, 'message' => string]
486
     * intval(code) === 0 means "no error".
487
     *
488
     * @return array{code: int|string|null, message: string|null}
489
     */
490
    public function error(): array
491
    {
492
        return [
54✔
493
            'code'    => $this->connID->lastErrorCode(),
54✔
494
            'message' => $this->connID->lastErrorMsg(),
54✔
495
        ];
54✔
496
    }
497

498
    /**
499
     * Insert ID
500
     */
501
    public function insertID(): int
502
    {
503
        return $this->connID->lastInsertRowID();
90✔
504
    }
505

506
    /**
507
     * Begin Transaction
508
     */
509
    protected function _transBegin(): bool
510
    {
511
        return $this->connID->exec('BEGIN TRANSACTION');
91✔
512
    }
513

514
    /**
515
     * Commit Transaction
516
     */
517
    protected function _transCommit(): bool
518
    {
519
        return $this->connID->exec('END TRANSACTION');
54✔
520
    }
521

522
    /**
523
     * Rollback Transaction
524
     */
525
    protected function _transRollback(): bool
526
    {
527
        return $this->connID->exec('ROLLBACK');
46✔
528
    }
529

530
    /**
531
     * Checks to see if the current install supports Foreign Keys
532
     * and has them enabled.
533
     */
534
    public function supportsForeignKeys(): bool
535
    {
536
        $result = $this->simpleQuery('PRAGMA foreign_keys');
36✔
537

538
        return (bool) $result;
36✔
539
    }
540
}
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