• 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

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\TableName;
19
use CodeIgniter\Exceptions\LogicException;
20
use mysqli;
21
use mysqli_result;
22
use mysqli_sql_exception;
23
use stdClass;
24
use Throwable;
25

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

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

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

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

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

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

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

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

100
    /**
101
     * Checks whether the native database error represents a unique constraint violation.
102
     */
103
    protected function isUniqueConstraintViolation(int|string $code, string $message): bool
104
    {
105
        // ER_DUP_ENTRY: duplicate key value.
106
        return $code === 1062;
52✔
107
    }
108

109
    /**
110
     * Checks whether the native database code represents a retryable transaction failure.
111
     */
112
    protected function isRetryableTransactionErrorCode(int|string $code): bool
113
    {
114
        // ER_LOCK_DEADLOCK: InnoDB rolls back the full transaction.
115
        return $code === 1213;
22✔
116
    }
117

118
    /**
119
     * Connect to the database.
120
     *
121
     * @return false|mysqli
122
     *
123
     * @throws DatabaseException
124
     */
125
    public function connect(bool $persistent = false)
126
    {
127
        // Do we have a socket path?
128
        if ($this->hostname[0] === '/') {
62✔
UNCOV
129
            $hostname = null;
×
UNCOV
130
            $port     = null;
×
UNCOV
131
            $socket   = $this->hostname;
×
132
        } else {
133
            $hostname = $persistent ? 'p:' . $this->hostname : $this->hostname;
62✔
134
            $port     = empty($this->port) ? null : $this->port;
62✔
135
            $socket   = '';
62✔
136
        }
137

138
        $clientFlags  = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
62✔
139
        $this->mysqli = mysqli_init();
62✔
140

141
        mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
62✔
142

143
        $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
62✔
144

145
        if ($this->numberNative === true) {
62✔
146
            $this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);
1✔
147
        }
148

149
        $initCommands = [];
62✔
150

151
        if ($this->strictOn) {
62✔
152
            $initCommands[] = "sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')";
62✔
153
        } else {
UNCOV
154
            $initCommands[] = "sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
×
155
                                @@sql_mode,
156
                                'STRICT_ALL_TABLES,', ''),
157
                            ',STRICT_ALL_TABLES', ''),
158
                        'STRICT_ALL_TABLES', ''),
159
                    'STRICT_TRANS_TABLES,', ''),
160
                ',STRICT_TRANS_TABLES', ''),
161
            'STRICT_TRANS_TABLES', '')";
×
162
        }
163

164
        // Set session timezone if configured
165
        $timezoneOffset = $this->getSessionTimezone();
62✔
166
        if ($timezoneOffset !== null) {
62✔
167
            $initCommands[] = "time_zone = '{$timezoneOffset}'";
3✔
168
        }
169

170
        $this->mysqli->options(
62✔
171
            MYSQLI_INIT_COMMAND,
62✔
172
            'SET SESSION ' . implode(', ', $initCommands),
62✔
173
        );
62✔
174

175
        if (is_array($this->encrypt)) {
62✔
UNCOV
176
            $ssl = [];
×
177

178
            if (! empty($this->encrypt['ssl_key'])) {
×
179
                $ssl['key'] = $this->encrypt['ssl_key'];
×
180
            }
181
            if (! empty($this->encrypt['ssl_cert'])) {
×
UNCOV
182
                $ssl['cert'] = $this->encrypt['ssl_cert'];
×
183
            }
UNCOV
184
            if (! empty($this->encrypt['ssl_ca'])) {
×
UNCOV
185
                $ssl['ca'] = $this->encrypt['ssl_ca'];
×
186
            }
UNCOV
187
            if (! empty($this->encrypt['ssl_capath'])) {
×
UNCOV
188
                $ssl['capath'] = $this->encrypt['ssl_capath'];
×
189
            }
190
            if (! empty($this->encrypt['ssl_cipher'])) {
×
191
                $ssl['cipher'] = $this->encrypt['ssl_cipher'];
×
192
            }
193

UNCOV
194
            if ($ssl !== []) {
×
195
                if (isset($this->encrypt['ssl_verify'])) {
×
196
                    if ($this->encrypt['ssl_verify']) {
×
197
                        if (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) {
×
198
                            $this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
×
199
                        }
200
                    }
201
                    // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
202
                    // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
203
                    // constant ...
204
                    //
205
                    // https://secure.php.net/ChangeLog-5.php#5.6.16
206
                    // https://bugs.php.net/bug.php?id=68344
UNCOV
207
                    elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>=')) {
×
UNCOV
208
                        $clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
×
209
                    }
210
                }
211

UNCOV
212
                $this->mysqli->ssl_set(
×
UNCOV
213
                    $ssl['key'] ?? null,
×
UNCOV
214
                    $ssl['cert'] ?? null,
×
UNCOV
215
                    $ssl['ca'] ?? null,
×
UNCOV
216
                    $ssl['capath'] ?? null,
×
UNCOV
217
                    $ssl['cipher'] ?? null,
×
UNCOV
218
                );
×
219
            }
220

UNCOV
221
            $clientFlags += MYSQLI_CLIENT_SSL;
×
222
        }
223

224
        if ($this->foundRows) {
62✔
225
            $clientFlags += MYSQLI_CLIENT_FOUND_ROWS;
1✔
226
        }
227

228
        try {
229
            if ($this->mysqli->real_connect(
62✔
230
                $hostname,
62✔
231
                $this->username,
62✔
232
                $this->password,
62✔
233
                $this->database,
62✔
234
                $port,
62✔
235
                $socket,
62✔
236
                $clientFlags,
62✔
237
            )) {
62✔
238
                if (! $this->mysqli->set_charset($this->charset)) {
62✔
UNCOV
239
                    log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}').");
×
240

UNCOV
241
                    $this->mysqli->close();
×
242

UNCOV
243
                    if ($this->DBDebug) {
×
UNCOV
244
                        throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
×
245
                    }
246

UNCOV
247
                    return false;
×
248
                }
249

250
                return $this->mysqli;
62✔
251
            }
252
        } catch (Throwable $e) {
1✔
253
            // Clean sensitive information from errors.
254
            $msg = $e->getMessage();
1✔
255

256
            $msg = str_replace($this->username, '****', $msg);
1✔
257
            $msg = str_replace($this->password, '****', $msg);
1✔
258

259
            throw new DatabaseException($msg, $e->getCode(), $e);
1✔
260
        }
261

UNCOV
262
        return false;
×
263
    }
264

265
    /**
266
     * Close the database connection.
267
     *
268
     * @return void
269
     */
270
    protected function _close()
271
    {
272
        $this->connID->close();
3✔
273
    }
274

275
    /**
276
     * Select a specific database table to use.
277
     */
278
    public function setDatabase(string $databaseName): bool
279
    {
UNCOV
280
        if ($databaseName === '') {
×
UNCOV
281
            $databaseName = $this->database;
×
282
        }
283

UNCOV
284
        if (empty($this->connID)) {
×
UNCOV
285
            $this->initialize();
×
286
        }
287

UNCOV
288
        if ($this->connID->select_db($databaseName)) {
×
UNCOV
289
            $this->database = $databaseName;
×
290

UNCOV
291
            return true;
×
292
        }
293

UNCOV
294
        return false;
×
295
    }
296

297
    /**
298
     * Returns a string containing the version of the database being used.
299
     */
300
    public function getVersion(): string
301
    {
302
        if (isset($this->dataCache['version'])) {
4✔
303
            return $this->dataCache['version'];
3✔
304
        }
305

306
        if (empty($this->mysqli)) {
2✔
307
            $this->initialize();
1✔
308
        }
309

310
        return $this->dataCache['version'] = $this->mysqli->server_info;
2✔
311
    }
312

313
    /**
314
     * Executes the query against the database.
315
     *
316
     * @return false|mysqli_result
317
     */
318
    protected function execute(string $sql)
319
    {
320
        while ($this->connID->more_results()) {
863✔
UNCOV
321
            $this->connID->next_result();
×
UNCOV
322
            if ($res = $this->connID->store_result()) {
×
UNCOV
323
                $res->free();
×
324
            }
325
        }
326

327
        try {
328
            return $this->connID->query($this->prepQuery($sql), $this->resultMode);
863✔
329
        } catch (mysqli_sql_exception $e) {
39✔
330
            log_message('error', "{message}\nin {exFile} on line {exLine}.\n{trace}", [
39✔
331
                'message' => $e->getMessage(),
39✔
332
                'exFile'  => clean_path($e->getFile()),
39✔
333
                'exLine'  => $e->getLine(),
39✔
334
                'trace'   => render_backtrace($e->getTrace()),
39✔
335
            ]);
39✔
336

337
            $exception = $this->createDatabaseException($e->getMessage(), $e->getCode(), $e);
39✔
338

339
            if ($this->DBDebug) {
39✔
340
                throw $exception;
17✔
341
            }
342

343
            $this->lastException = $exception;
22✔
344
        }
345

346
        return false;
22✔
347
    }
348

349
    /**
350
     * Prep the query. If needed, each database adapter can prep the query string
351
     */
352
    protected function prepQuery(string $sql): string
353
    {
354
        // mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
355
        // modifies the query so that it a proper number of affected rows is returned.
356
        if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) {
863✔
357
            return trim($sql) . ' WHERE 1=1';
3✔
358
        }
359

360
        return $sql;
863✔
361
    }
362

363
    /**
364
     * Returns the total number of rows affected by this query.
365
     */
366
    public function affectedRows(): int
367
    {
368
        return $this->connID->affected_rows ?? 0;
62✔
369
    }
370

371
    /**
372
     * Platform-dependant string escape
373
     */
374
    protected function _escapeString(string $str): string
375
    {
376
        if (! $this->connID) {
834✔
UNCOV
377
            $this->initialize();
×
378
        }
379

380
        return $this->connID->real_escape_string($str);
834✔
381
    }
382

383
    /**
384
     * Escape Like String Direct
385
     * There are a few instances where MySQLi queries cannot take the
386
     * additional "ESCAPE x" parameter for specifying the escape character
387
     * in "LIKE" strings, and this handles those directly with a backslash.
388
     *
389
     * @param list<string>|string $str Input string
390
     *
391
     * @return list<string>|string
392
     */
393
    public function escapeLikeStringDirect($str)
394
    {
395
        if (is_array($str)) {
1✔
UNCOV
396
            foreach ($str as $key => $val) {
×
UNCOV
397
                $str[$key] = $this->escapeLikeStringDirect($val);
×
398
            }
399

UNCOV
400
            return $str;
×
401
        }
402

403
        $str = $this->_escapeString($str);
1✔
404

405
        // Escape LIKE condition wildcards
406
        return str_replace(
1✔
407
            [$this->likeEscapeChar, '%', '_'],
1✔
408
            ['\\' . $this->likeEscapeChar, '\\%', '\\_'],
1✔
409
            $str,
1✔
410
        );
1✔
411
    }
412

413
    /**
414
     * Generates the SQL for listing tables in a platform-dependent manner.
415
     * Uses escapeLikeStringDirect().
416
     *
417
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
418
     */
419
    protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
420
    {
421
        $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
778✔
422

423
        if ((string) $tableName !== '') {
778✔
424
            return $sql . ' LIKE ' . $this->escape($tableName);
777✔
425
        }
426

427
        if ($prefixLimit && $this->DBPrefix !== '') {
61✔
UNCOV
428
            return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
×
429
        }
430

431
        return $sql;
61✔
432
    }
433

434
    /**
435
     * Generates a platform-specific query string so that the column names can be fetched.
436
     *
437
     * @param string|TableName $table
438
     */
439
    protected function _listColumns($table = ''): string
440
    {
441
        $tableName = $this->protectIdentifiers(
8✔
442
            $table,
8✔
443
            true,
8✔
444
            null,
8✔
445
            false,
8✔
446
        );
8✔
447

448
        return 'SHOW COLUMNS FROM ' . $tableName;
8✔
449
    }
450

451
    /**
452
     * Returns an array of objects with field data
453
     *
454
     * @return list<stdClass>
455
     *
456
     * @throws DatabaseException
457
     */
458
    protected function _fieldData(string $table): array
459
    {
460
        $table = $this->protectIdentifiers($table, true, null, false);
14✔
461

462
        if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
14✔
UNCOV
463
            throw new DatabaseException(lang('Database.failGetFieldData'));
×
464
        }
465
        $query = $query->getResultObject();
14✔
466

467
        $retVal = [];
14✔
468

469
        for ($i = 0, $c = count($query); $i < $c; $i++) {
14✔
470
            $retVal[$i]       = new stdClass();
14✔
471
            $retVal[$i]->name = $query[$i]->Field;
14✔
472

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

475
            $retVal[$i]->nullable    = $query[$i]->Null === 'YES';
14✔
476
            $retVal[$i]->default     = $query[$i]->Default;
14✔
477
            $retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
14✔
478
        }
479

480
        return $retVal;
14✔
481
    }
482

483
    /**
484
     * Returns an array of objects with index data
485
     *
486
     * @return array<string, stdClass>
487
     *
488
     * @throws DatabaseException
489
     * @throws LogicException
490
     */
491
    protected function _indexData(string $table): array
492
    {
493
        $table = $this->protectIdentifiers($table, true, null, false);
8✔
494

495
        if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
8✔
UNCOV
496
            throw new DatabaseException(lang('Database.failGetIndexData'));
×
497
        }
498

499
        $indexes = $query->getResultArray();
8✔
500

501
        if ($indexes === []) {
8✔
502
            return [];
3✔
503
        }
504

505
        $keys = [];
7✔
506

507
        foreach ($indexes as $index) {
7✔
508
            if (empty($keys[$index['Key_name']])) {
7✔
509
                $keys[$index['Key_name']]       = new stdClass();
7✔
510
                $keys[$index['Key_name']]->name = $index['Key_name'];
7✔
511

512
                if ($index['Key_name'] === 'PRIMARY') {
7✔
513
                    $type = 'PRIMARY';
5✔
514
                } elseif ($index['Index_type'] === 'FULLTEXT') {
5✔
UNCOV
515
                    $type = 'FULLTEXT';
×
516
                } elseif ($index['Non_unique']) {
5✔
517
                    $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
5✔
518
                } else {
519
                    $type = 'UNIQUE';
3✔
520
                }
521

522
                $keys[$index['Key_name']]->type = $type;
7✔
523
            }
524

525
            $keys[$index['Key_name']]->fields[] = $index['Column_name'];
7✔
526
        }
527

528
        return $keys;
7✔
529
    }
530

531
    /**
532
     * Returns an array of objects with Foreign key data
533
     *
534
     * @return array<string, stdClass>
535
     *
536
     * @throws DatabaseException
537
     */
538
    protected function _foreignKeyData(string $table): array
539
    {
540
        $sql = '
6✔
541
                SELECT
542
                    tc.CONSTRAINT_NAME,
543
                    tc.TABLE_NAME,
544
                    kcu.COLUMN_NAME,
545
                    rc.REFERENCED_TABLE_NAME,
546
                    kcu.REFERENCED_COLUMN_NAME,
547
                    rc.DELETE_RULE,
548
                    rc.UPDATE_RULE,
549
                    rc.MATCH_OPTION
550
                FROM information_schema.table_constraints AS tc
551
                INNER JOIN information_schema.referential_constraints AS rc
552
                    ON tc.constraint_name = rc.constraint_name
553
                    AND tc.constraint_schema = rc.constraint_schema
554
                INNER JOIN information_schema.key_column_usage AS kcu
555
                    ON tc.constraint_name = kcu.constraint_name
556
                    AND tc.constraint_schema = kcu.constraint_schema
557
                WHERE
558
                    tc.constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND
6✔
559
                    tc.table_schema = ' . $this->escape($this->database) . ' AND
6✔
560
                    tc.table_name = ' . $this->escape($table);
6✔
561

562
        if (($query = $this->query($sql)) === false) {
6✔
UNCOV
563
            throw new DatabaseException(lang('Database.failGetForeignKeyData'));
×
564
        }
565

566
        $query   = $query->getResultObject();
6✔
567
        $indexes = [];
6✔
568

569
        foreach ($query as $row) {
6✔
570
            $indexes[$row->CONSTRAINT_NAME]['constraint_name']       = $row->CONSTRAINT_NAME;
5✔
571
            $indexes[$row->CONSTRAINT_NAME]['table_name']            = $row->TABLE_NAME;
5✔
572
            $indexes[$row->CONSTRAINT_NAME]['column_name'][]         = $row->COLUMN_NAME;
5✔
573
            $indexes[$row->CONSTRAINT_NAME]['foreign_table_name']    = $row->REFERENCED_TABLE_NAME;
5✔
574
            $indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME;
5✔
575
            $indexes[$row->CONSTRAINT_NAME]['on_delete']             = $row->DELETE_RULE;
5✔
576
            $indexes[$row->CONSTRAINT_NAME]['on_update']             = $row->UPDATE_RULE;
5✔
577
            $indexes[$row->CONSTRAINT_NAME]['match']                 = $row->MATCH_OPTION;
5✔
578
        }
579

580
        return $this->foreignKeyDataToObjects($indexes);
6✔
581
    }
582

583
    /**
584
     * Returns platform-specific SQL to disable foreign key checks.
585
     *
586
     * @return string
587
     */
588
    protected function _disableForeignKeyChecks()
589
    {
590
        return 'SET FOREIGN_KEY_CHECKS=0';
783✔
591
    }
592

593
    /**
594
     * Returns platform-specific SQL to enable foreign key checks.
595
     *
596
     * @return string
597
     */
598
    protected function _enableForeignKeyChecks()
599
    {
600
        return 'SET FOREIGN_KEY_CHECKS=1';
783✔
601
    }
602

603
    /**
604
     * Returns the last error code and message.
605
     * Must return this format: ['code' => string|int, 'message' => string]
606
     * intval(code) === 0 means "no error".
607
     *
608
     * @return array{code: int|string|null, message: string|null}
609
     */
610
    public function error(): array
611
    {
612
        if (! empty($this->mysqli->connect_errno)) {
3✔
UNCOV
613
            return [
×
UNCOV
614
                'code'    => $this->mysqli->connect_errno,
×
UNCOV
615
                'message' => $this->mysqli->connect_error,
×
UNCOV
616
            ];
×
617
        }
618

619
        return [
3✔
620
            'code'    => $this->connID->errno,
3✔
621
            'message' => $this->connID->error,
3✔
622
        ];
3✔
623
    }
624

625
    /**
626
     * Insert ID
627
     */
628
    public function insertID(): int
629
    {
630
        return $this->connID->insert_id;
88✔
631
    }
632

633
    /**
634
     * Begin Transaction
635
     */
636
    protected function _transBegin(): bool
637
    {
638
        return $this->connID->begin_transaction();
44✔
639
    }
640

641
    /**
642
     * Commit Transaction
643
     */
644
    protected function _transCommit(): bool
645
    {
646
        return $this->connID->commit();
16✔
647
    }
648

649
    /**
650
     * Rollback Transaction
651
     */
652
    protected function _transRollback(): bool
653
    {
654
        return $this->connID->rollback();
33✔
655
    }
656
}
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