• 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

70.39
/system/Database/MySQLi/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\MySQLi;
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\LogicException;
21
use mysqli;
22
use mysqli_result;
23
use mysqli_sql_exception;
24
use stdClass;
25
use Throwable;
26

27
/**
28
 * Connection for MySQLi
29
 *
30
 * @extends BaseConnection<mysqli, mysqli_result>
31
 */
32
class Connection extends BaseConnection
33
{
34
    /**
35
     * Database driver
36
     *
37
     * @var string
38
     */
39
    public $DBDriver = 'MySQLi';
40

41
    /**
42
     * DELETE hack flag
43
     *
44
     * Whether to use the MySQL "delete hack" which allows the number
45
     * of affected rows to be shown. Uses a preg_replace when enabled,
46
     * adding a bit more processing to all queries.
47
     *
48
     * @var bool
49
     */
50
    public $deleteHack = true;
51

52
    /**
53
     * Identifier escape character
54
     *
55
     * @var string
56
     */
57
    public $escapeChar = '`';
58

59
    /**
60
     * MySQLi object
61
     *
62
     * Has to be preserved without being assigned to $connId.
63
     *
64
     * @var false|mysqli
65
     */
66
    public $mysqli;
67

68
    /**
69
     * MySQLi constant
70
     *
71
     * For unbuffered queries use `MYSQLI_USE_RESULT`.
72
     *
73
     * Default mode for buffered queries uses `MYSQLI_STORE_RESULT`.
74
     *
75
     * @var int
76
     */
77
    public $resultMode = MYSQLI_STORE_RESULT;
78

79
    /**
80
     * Use MYSQLI_OPT_INT_AND_FLOAT_NATIVE
81
     *
82
     * @var bool
83
     */
84
    public $numberNative = false;
85

86
    /**
87
     * Use MYSQLI_CLIENT_FOUND_ROWS
88
     *
89
     * Whether affectedRows() should return number of rows found,
90
     * or number of rows changed, after an UPDATE query.
91
     *
92
     * @var bool
93
     */
94
    public $foundRows = false;
95

96
    /**
97
     * Strict SQL mode
98
     */
99
    protected bool $strictOn = false;
100

101
    /**
102
     * Connect to the database.
103
     *
104
     * @return false|mysqli
105
     *
106
     * @throws DatabaseException
107
     */
108
    public function connect(bool $persistent = false)
109
    {
110
        // Do we have a socket path?
111
        if ($this->hostname[0] === '/') {
35✔
112
            $hostname = null;
×
113
            $port     = null;
×
UNCOV
114
            $socket   = $this->hostname;
×
115
        } else {
116
            $hostname = $persistent ? 'p:' . $this->hostname : $this->hostname;
35✔
117
            $port     = empty($this->port) ? null : $this->port;
35✔
118
            $socket   = '';
35✔
119
        }
120

121
        $clientFlags  = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
35✔
122
        $this->mysqli = mysqli_init();
35✔
123

124
        mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
35✔
125

126
        $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
35✔
127

128
        if ($this->numberNative === true) {
35✔
129
            $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);
1✔
130
        }
131

132
        $initCommands = [];
35✔
133

134
        if ($this->strictOn) {
35✔
135
            $initCommands[] = "sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')";
35✔
136
        } else {
NEW
137
            $initCommands[] = "sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
×
138
                                @@sql_mode,
139
                                'STRICT_ALL_TABLES,', ''),
140
                            ',STRICT_ALL_TABLES', ''),
141
                        'STRICT_ALL_TABLES', ''),
142
                    'STRICT_TRANS_TABLES,', ''),
143
                ',STRICT_TRANS_TABLES', ''),
NEW
144
            'STRICT_TRANS_TABLES', '')";
×
145
        }
146

147
        // Set session timezone if configured
148
        $timezoneOffset = $this->getSessionTimezone();
35✔
149
        if ($timezoneOffset !== null) {
35✔
150
            $initCommands[] = "time_zone = '{$timezoneOffset}'";
3✔
151
        }
152

153
        $this->mysqli->options(
35✔
154
            MYSQLI_INIT_COMMAND,
35✔
155
            'SET SESSION ' . implode(', ', $initCommands),
35✔
156
        );
35✔
157

158
        if (is_array($this->encrypt)) {
35✔
UNCOV
159
            $ssl = [];
×
160

161
            if (! empty($this->encrypt['ssl_key'])) {
×
UNCOV
162
                $ssl['key'] = $this->encrypt['ssl_key'];
×
163
            }
164
            if (! empty($this->encrypt['ssl_cert'])) {
×
UNCOV
165
                $ssl['cert'] = $this->encrypt['ssl_cert'];
×
166
            }
167
            if (! empty($this->encrypt['ssl_ca'])) {
×
UNCOV
168
                $ssl['ca'] = $this->encrypt['ssl_ca'];
×
169
            }
170
            if (! empty($this->encrypt['ssl_capath'])) {
×
UNCOV
171
                $ssl['capath'] = $this->encrypt['ssl_capath'];
×
172
            }
173
            if (! empty($this->encrypt['ssl_cipher'])) {
×
UNCOV
174
                $ssl['cipher'] = $this->encrypt['ssl_cipher'];
×
175
            }
176

177
            if ($ssl !== []) {
×
178
                if (isset($this->encrypt['ssl_verify'])) {
×
179
                    if ($this->encrypt['ssl_verify']) {
×
180
                        if (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) {
×
UNCOV
181
                            $this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
×
182
                        }
183
                    }
184
                    // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
185
                    // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
186
                    // constant ...
187
                    //
188
                    // https://secure.php.net/ChangeLog-5.php#5.6.16
189
                    // https://bugs.php.net/bug.php?id=68344
190
                    elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>=')) {
×
UNCOV
191
                        $clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
×
192
                    }
193
                }
194

195
                $this->mysqli->ssl_set(
×
196
                    $ssl['key'] ?? null,
×
197
                    $ssl['cert'] ?? null,
×
198
                    $ssl['ca'] ?? null,
×
199
                    $ssl['capath'] ?? null,
×
200
                    $ssl['cipher'] ?? null,
×
UNCOV
201
                );
×
202
            }
203

UNCOV
204
            $clientFlags += MYSQLI_CLIENT_SSL;
×
205
        }
206

207
        if ($this->foundRows) {
35✔
208
            $clientFlags += MYSQLI_CLIENT_FOUND_ROWS;
1✔
209
        }
210

211
        try {
212
            if ($this->mysqli->real_connect(
35✔
213
                $hostname,
35✔
214
                $this->username,
35✔
215
                $this->password,
35✔
216
                $this->database,
35✔
217
                $port,
35✔
218
                $socket,
35✔
219
                $clientFlags,
35✔
220
            )) {
35✔
221
                if (! $this->mysqli->set_charset($this->charset)) {
35✔
UNCOV
222
                    log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}').");
×
223

UNCOV
224
                    $this->mysqli->close();
×
225

226
                    if ($this->DBDebug) {
×
UNCOV
227
                        throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
×
228
                    }
229

UNCOV
230
                    return false;
×
231
                }
232

233
                return $this->mysqli;
35✔
234
            }
235
        } catch (Throwable $e) {
1✔
236
            // Clean sensitive information from errors.
237
            $msg = $e->getMessage();
1✔
238

239
            $msg = str_replace($this->username, '****', $msg);
1✔
240
            $msg = str_replace($this->password, '****', $msg);
1✔
241

242
            throw new DatabaseException($msg, $e->getCode(), $e);
1✔
243
        }
244

UNCOV
245
        return false;
×
246
    }
247

248
    /**
249
     * Close the database connection.
250
     *
251
     * @return void
252
     */
253
    protected function _close()
254
    {
255
        $this->connID->close();
3✔
256
    }
257

258
    /**
259
     * Select a specific database table to use.
260
     */
261
    public function setDatabase(string $databaseName): bool
262
    {
263
        if ($databaseName === '') {
×
UNCOV
264
            $databaseName = $this->database;
×
265
        }
266

267
        if (empty($this->connID)) {
×
UNCOV
268
            $this->initialize();
×
269
        }
270

271
        if ($this->connID->select_db($databaseName)) {
×
UNCOV
272
            $this->database = $databaseName;
×
273

UNCOV
274
            return true;
×
275
        }
276

UNCOV
277
        return false;
×
278
    }
279

280
    /**
281
     * Returns a string containing the version of the database being used.
282
     */
283
    public function getVersion(): string
284
    {
285
        if (isset($this->dataCache['version'])) {
4✔
286
            return $this->dataCache['version'];
3✔
287
        }
288

289
        if (empty($this->mysqli)) {
2✔
290
            $this->initialize();
1✔
291
        }
292

293
        return $this->dataCache['version'] = $this->mysqli->server_info;
2✔
294
    }
295

296
    /**
297
     * Executes the query against the database.
298
     *
299
     * @return false|mysqli_result
300
     */
301
    protected function execute(string $sql)
302
    {
303
        while ($this->connID->more_results()) {
804✔
304
            $this->connID->next_result();
×
305
            if ($res = $this->connID->store_result()) {
×
UNCOV
306
                $res->free();
×
307
            }
308
        }
309

310
        try {
311
            return $this->connID->query($this->prepQuery($sql), $this->resultMode);
804✔
312
        } catch (mysqli_sql_exception $e) {
34✔
313
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
34✔
314
                'message' => $e->getMessage(),
34✔
315
                'exFile'  => clean_path($e->getFile()),
34✔
316
                'exLine'  => $e->getLine(),
34✔
317
                'trace'   => render_backtrace($e->getTrace()),
34✔
318
            ]);
34✔
319

320
            // MySQL error 1062: ER_DUP_ENTRY – duplicate key value
321
            $exception = $e->getCode() === 1062
34✔
322
                ? new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e)
20✔
323
                : new DatabaseException($e->getMessage(), $e->getCode(), $e);
14✔
324

325
            if ($this->DBDebug) {
34✔
326
                throw $exception;
15✔
327
            }
328

329
            $this->lastException = $exception;
19✔
330
        }
331

332
        return false;
19✔
333
    }
334

335
    /**
336
     * Prep the query. If needed, each database adapter can prep the query string
337
     */
338
    protected function prepQuery(string $sql): string
339
    {
340
        // mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
341
        // modifies the query so that it a proper number of affected rows is returned.
342
        if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) {
804✔
343
            return trim($sql) . ' WHERE 1=1';
3✔
344
        }
345

346
        return $sql;
804✔
347
    }
348

349
    /**
350
     * Returns the total number of rows affected by this query.
351
     */
352
    public function affectedRows(): int
353
    {
354
        return $this->connID->affected_rows ?? 0;
62✔
355
    }
356

357
    /**
358
     * Platform-dependant string escape
359
     */
360
    protected function _escapeString(string $str): string
361
    {
362
        if (! $this->connID) {
775✔
UNCOV
363
            $this->initialize();
×
364
        }
365

366
        return $this->connID->real_escape_string($str);
775✔
367
    }
368

369
    /**
370
     * Escape Like String Direct
371
     * There are a few instances where MySQLi queries cannot take the
372
     * additional "ESCAPE x" parameter for specifying the escape character
373
     * in "LIKE" strings, and this handles those directly with a backslash.
374
     *
375
     * @param list<string>|string $str Input string
376
     *
377
     * @return list<string>|string
378
     */
379
    public function escapeLikeStringDirect($str)
380
    {
381
        if (is_array($str)) {
1✔
UNCOV
382
            foreach ($str as $key => $val) {
×
UNCOV
383
                $str[$key] = $this->escapeLikeStringDirect($val);
×
384
            }
385

UNCOV
386
            return $str;
×
387
        }
388

389
        $str = $this->_escapeString($str);
1✔
390

391
        // Escape LIKE condition wildcards
392
        return str_replace(
1✔
393
            [$this->likeEscapeChar, '%', '_'],
1✔
394
            ['\\' . $this->likeEscapeChar, '\\%', '\\_'],
1✔
395
            $str,
1✔
396
        );
1✔
397
    }
398

399
    /**
400
     * Generates the SQL for listing tables in a platform-dependent manner.
401
     * Uses escapeLikeStringDirect().
402
     *
403
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
404
     */
405
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
406
    {
407
        $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
719✔
408

409
        if ((string) $tableName !== '') {
719✔
410
            return $sql . ' LIKE ' . $this->escape($tableName);
718✔
411
        }
412

413
        if ($prefixLimit && $this->DBPrefix !== '') {
34✔
UNCOV
414
            return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
×
415
        }
416

417
        return $sql;
34✔
418
    }
419

420
    /**
421
     * Generates a platform-specific query string so that the column names can be fetched.
422
     *
423
     * @param string|TableName $table
424
     */
425
    protected function _listColumns($table = ''): string
426
    {
427
        $tableName = $this->protectIdentifiers(
8✔
428
            $table,
8✔
429
            true,
8✔
430
            null,
8✔
431
            false,
8✔
432
        );
8✔
433

434
        return 'SHOW COLUMNS FROM ' . $tableName;
8✔
435
    }
436

437
    /**
438
     * Returns an array of objects with field data
439
     *
440
     * @return list<stdClass>
441
     *
442
     * @throws DatabaseException
443
     */
444
    protected function _fieldData(string $table): array
445
    {
446
        $table = $this->protectIdentifiers($table, true, null, false);
14✔
447

448
        if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
14✔
UNCOV
449
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
450
        }
451
        $query = $query->getResultObject();
14✔
452

453
        $retVal = [];
14✔
454

455
        for ($i = 0, $c = count($query); $i < $c; $i++) {
14✔
456
            $retVal[$i]       = new stdClass();
14✔
457
            $retVal[$i]->name = $query[$i]->Field;
14✔
458

459
            sscanf($query[$i]->Type, '%[a-z](%d)', $retVal[$i]->type, $retVal[$i]->max_length);
14✔
460

461
            $retVal[$i]->nullable    = $query[$i]->Null === 'YES';
14✔
462
            $retVal[$i]->default     = $query[$i]->Default;
14✔
463
            $retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
14✔
464
        }
465

466
        return $retVal;
14✔
467
    }
468

469
    /**
470
     * Returns an array of objects with index data
471
     *
472
     * @return array<string, stdClass>
473
     *
474
     * @throws DatabaseException
475
     * @throws LogicException
476
     */
477
    protected function _indexData(string $table): array
478
    {
479
        $table = $this->protectIdentifiers($table, true, null, false);
8✔
480

481
        if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
8✔
UNCOV
482
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
483
        }
484

485
        $indexes = $query->getResultArray();
8✔
486

487
        if ($indexes === []) {
8✔
488
            return [];
3✔
489
        }
490

491
        $keys = [];
7✔
492

493
        foreach ($indexes as $index) {
7✔
494
            if (empty($keys[$index['Key_name']])) {
7✔
495
                $keys[$index['Key_name']]       = new stdClass();
7✔
496
                $keys[$index['Key_name']]->name = $index['Key_name'];
7✔
497

498
                if ($index['Key_name'] === 'PRIMARY') {
7✔
499
                    $type = 'PRIMARY';
5✔
500
                } elseif ($index['Index_type'] === 'FULLTEXT') {
5✔
UNCOV
501
                    $type = 'FULLTEXT';
×
502
                } elseif ($index['Non_unique']) {
5✔
503
                    $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
5✔
504
                } else {
505
                    $type = 'UNIQUE';
3✔
506
                }
507

508
                $keys[$index['Key_name']]->type = $type;
7✔
509
            }
510

511
            $keys[$index['Key_name']]->fields[] = $index['Column_name'];
7✔
512
        }
513

514
        return $keys;
7✔
515
    }
516

517
    /**
518
     * Returns an array of objects with Foreign key data
519
     *
520
     * @return array<string, stdClass>
521
     *
522
     * @throws DatabaseException
523
     */
524
    protected function _foreignKeyData(string $table): array
525
    {
526
        $sql = '
6✔
527
                SELECT
528
                    tc.CONSTRAINT_NAME,
529
                    tc.TABLE_NAME,
530
                    kcu.COLUMN_NAME,
531
                    rc.REFERENCED_TABLE_NAME,
532
                    kcu.REFERENCED_COLUMN_NAME,
533
                    rc.DELETE_RULE,
534
                    rc.UPDATE_RULE,
535
                    rc.MATCH_OPTION
536
                FROM information_schema.table_constraints AS tc
537
                INNER JOIN information_schema.referential_constraints AS rc
538
                    ON tc.constraint_name = rc.constraint_name
539
                    AND tc.constraint_schema = rc.constraint_schema
540
                INNER JOIN information_schema.key_column_usage AS kcu
541
                    ON tc.constraint_name = kcu.constraint_name
542
                    AND tc.constraint_schema = kcu.constraint_schema
543
                WHERE
544
                    tc.constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND
6✔
545
                    tc.table_schema = ' . $this->escape($this->database) . ' AND
6✔
546
                    tc.table_name = ' . $this->escape($table);
6✔
547

548
        if (($query = $this->query($sql)) === false) {
6✔
UNCOV
549
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
550
        }
551

552
        $query   = $query->getResultObject();
6✔
553
        $indexes = [];
6✔
554

555
        foreach ($query as $row) {
6✔
556
            $indexes[$row->CONSTRAINT_NAME]['constraint_name']       = $row->CONSTRAINT_NAME;
5✔
557
            $indexes[$row->CONSTRAINT_NAME]['table_name']            = $row->TABLE_NAME;
5✔
558
            $indexes[$row->CONSTRAINT_NAME]['column_name'][]         = $row->COLUMN_NAME;
5✔
559
            $indexes[$row->CONSTRAINT_NAME]['foreign_table_name']    = $row->REFERENCED_TABLE_NAME;
5✔
560
            $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME;
5✔
561
            $indexes[$row->CONSTRAINT_NAME]['on_delete']             = $row->DELETE_RULE;
5✔
562
            $indexes[$row->CONSTRAINT_NAME]['on_update']             = $row->UPDATE_RULE;
5✔
563
            $indexes[$row->CONSTRAINT_NAME]['match']                 = $row->MATCH_OPTION;
5✔
564
        }
565

566
        return $this->foreignKeyDataToObjects($indexes);
6✔
567
    }
568

569
    /**
570
     * Returns platform-specific SQL to disable foreign key checks.
571
     *
572
     * @return string
573
     */
574
    protected function _disableForeignKeyChecks()
575
    {
576
        return 'SET FOREIGN_KEY_CHECKS=0';
724✔
577
    }
578

579
    /**
580
     * Returns platform-specific SQL to enable foreign key checks.
581
     *
582
     * @return string
583
     */
584
    protected function _enableForeignKeyChecks()
585
    {
586
        return 'SET FOREIGN_KEY_CHECKS=1';
724✔
587
    }
588

589
    /**
590
     * Returns the last error code and message.
591
     * Must return this format: ['code' => string|int, 'message' => string]
592
     * intval(code) === 0 means "no error".
593
     *
594
     * @return array<string, int|string>
595
     */
596
    public function error(): array
597
    {
598
        if (! empty($this->mysqli->connect_errno)) {
3✔
UNCOV
599
            return [
×
UNCOV
600
                'code'    => $this->mysqli->connect_errno,
×
UNCOV
601
                'message' => $this->mysqli->connect_error,
×
UNCOV
602
            ];
×
603
        }
604

605
        return [
3✔
606
            'code'    => $this->connID->errno,
3✔
607
            'message' => $this->connID->error,
3✔
608
        ];
3✔
609
    }
610

611
    /**
612
     * Insert ID
613
     */
614
    public function insertID(): int
615
    {
616
        return $this->connID->insert_id;
83✔
617
    }
618

619
    /**
620
     * Begin Transaction
621
     */
622
    protected function _transBegin(): bool
623
    {
624
        return $this->connID->begin_transaction();
19✔
625
    }
626

627
    /**
628
     * Commit Transaction
629
     */
630
    protected function _transCommit(): bool
631
    {
632
        return $this->connID->commit();
5✔
633
    }
634

635
    /**
636
     * Rollback Transaction
637
     */
638
    protected function _transRollback(): bool
639
    {
640
        return $this->connID->rollback();
19✔
641
    }
642
}
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