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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

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

89.05
/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();
669✔
75

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

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

84
        if (is_int($this->synchronous)) {
669✔
85
            if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
1✔
86
                throw new InvalidArgumentException('Invalid synchronous value.');
1✔
87
            }
88
            $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
×
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) {
28✔
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)) {
28✔
107
                $this->database = WRITEPATH . $this->database;
25✔
108
            }
109

110
            $sqlite = (! isset($this->password) || $this->password !== '')
28✔
111
                ? new SQLite3($this->database)
3✔
112
                : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
25✔
113

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

116
            return $sqlite;
28✔
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'])) {
678✔
158
            return $this->dataCache['version'];
676✔
159
        }
160

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

163
        return $this->dataCache['version'] = $version['versionString'];
53✔
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)
679✔
175
                ? $this->connID->exec($sql)
645✔
176
                : $this->connID->query($sql);
679✔
177
        } catch (Exception $e) {
29✔
178
            log_message('error', (string) $e);
29✔
179

180
            if ($this->DBDebug) {
29✔
181
                throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
12✔
182
            }
183
        }
184

185
        return false;
17✔
186
    }
187

188
    /**
189
     * Returns the total number of rows affected by this query.
190
     */
191
    public function affectedRows(): int
192
    {
193
        return $this->connID->changes();
47✔
194
    }
195

196
    /**
197
     * Platform-dependant string escape
198
     */
199
    protected function _escapeString(string $str): string
200
    {
201
        if (! $this->connID instanceof SQLite3) {
650✔
202
            $this->initialize();
×
203
        }
204

205
        return $this->connID->escapeString($str);
650✔
206
    }
207

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

221
        return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
50✔
222
               . ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
50✔
223
               . (($prefixLimit && $this->DBPrefix !== '')
50✔
224
                    ? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
×
225
                    : '');
50✔
226
    }
227

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

241
        return 'PRAGMA TABLE_INFO(' . $tableName . ')';
12✔
242
    }
243

244
    /**
245
     * @param string|TableName $tableName
246
     *
247
     * @return false|list<string>
248
     *
249
     * @throws DatabaseException
250
     */
251
    public function getFieldNames($tableName)
252
    {
253
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
16✔
254

255
        // Is there a cached result?
256
        if (isset($this->dataCache['field_names'][$table])) {
16✔
257
            return $this->dataCache['field_names'][$table];
9✔
258
        }
259

260
        if (! $this->connID instanceof SQLite3) {
12✔
261
            $this->initialize();
×
262
        }
263

264
        $sql = $this->_listColumns($tableName);
12✔
265

266
        $query                                  = $this->query($sql);
12✔
267
        $this->dataCache['field_names'][$table] = [];
12✔
268

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

284
            $this->dataCache['field_names'][$table][] = $row[$key];
12✔
285
        }
286

287
        return $this->dataCache['field_names'][$table];
12✔
288
    }
289

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

303
        $query = $query->getResultObject();
38✔
304

305
        if (empty($query)) {
38✔
306
            return [];
×
307
        }
308

309
        $retVal = [];
38✔
310

311
        for ($i = 0, $c = count($query); $i < $c; $i++) {
38✔
312
            $retVal[$i] = new stdClass();
38✔
313

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

325
        return $retVal;
38✔
326
    }
327

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

353
        if (($query = $this->query($sql)) === false) {
47✔
354
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
355
        }
356
        $query = $query->getResultObject();
47✔
357

358
        $tempVal = [];
47✔
359

360
        foreach ($query as $row) {
47✔
361
            if ($row->indextype === 'PRIMARY') {
33✔
362
                $tempVal['PRIMARY']['indextype']               = $row->indextype;
31✔
363
                $tempVal['PRIMARY']['indexname']               = $row->indexname;
31✔
364
                $tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
31✔
365
            } else {
366
                $tempVal[$row->indexname]['indextype']               = $row->indextype;
24✔
367
                $tempVal[$row->indexname]['indexname']               = $row->indexname;
24✔
368
                $tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
24✔
369
            }
370
        }
371

372
        $retVal = [];
47✔
373

374
        foreach ($tempVal as $val) {
47✔
375
            $obj                = new stdClass();
33✔
376
            $obj->name          = $val['indexname'];
33✔
377
            $obj->fields        = array_values($val['fields']);
33✔
378
            $obj->type          = $val['indextype'];
33✔
379
            $retVal[$obj->name] = $obj;
33✔
380
        }
381

382
        return $retVal;
47✔
383
    }
384

385
    /**
386
     * Returns an array of objects with Foreign key data
387
     *
388
     * @return array<string, stdClass>
389
     */
390
    protected function _foreignKeyData(string $table): array
391
    {
392
        if (! $this->supportsForeignKeys()) {
33✔
393
            return [];
×
394
        }
395

396
        $query   = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
33✔
397
        $indexes = [];
33✔
398

399
        foreach ($query as $row) {
33✔
400
            $indexes[$row->id]['constraint_name']       = null;
11✔
401
            $indexes[$row->id]['table_name']            = $table;
11✔
402
            $indexes[$row->id]['foreign_table_name']    = $row->table;
11✔
403
            $indexes[$row->id]['column_name'][]         = $row->from;
11✔
404
            $indexes[$row->id]['foreign_column_name'][] = $row->to;
11✔
405
            $indexes[$row->id]['on_delete']             = $row->on_delete;
11✔
406
            $indexes[$row->id]['on_update']             = $row->on_update;
11✔
407
            $indexes[$row->id]['match']                 = $row->match;
11✔
408
        }
409

410
        return $this->foreignKeyDataToObjects($indexes);
33✔
411
    }
412

413
    /**
414
     * Returns platform-specific SQL to disable foreign key checks.
415
     *
416
     * @return string
417
     */
418
    protected function _disableForeignKeyChecks()
419
    {
420
        return 'PRAGMA foreign_keys = OFF';
603✔
421
    }
422

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

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

448
    /**
449
     * Insert ID
450
     */
451
    public function insertID(): int
452
    {
453
        return $this->connID->lastInsertRowID();
79✔
454
    }
455

456
    /**
457
     * Begin Transaction
458
     */
459
    protected function _transBegin(): bool
460
    {
461
        return $this->connID->exec('BEGIN TRANSACTION');
47✔
462
    }
463

464
    /**
465
     * Commit Transaction
466
     */
467
    protected function _transCommit(): bool
468
    {
469
        return $this->connID->exec('END TRANSACTION');
35✔
470
    }
471

472
    /**
473
     * Rollback Transaction
474
     */
475
    protected function _transRollback(): bool
476
    {
477
        return $this->connID->exec('ROLLBACK');
17✔
478
    }
479

480
    /**
481
     * Checks to see if the current install supports Foreign Keys
482
     * and has them enabled.
483
     */
484
    public function supportsForeignKeys(): bool
485
    {
486
        $result = $this->simpleQuery('PRAGMA foreign_keys');
33✔
487

488
        return (bool) $result;
33✔
489
    }
490
}
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

© 2025 Coveralls, Inc