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

codeigniter4 / CodeIgniter4 / 39872993952

22 Oct 2026 11:37PM UTC coverage: 90.196% (+0.01%) from 90.183%
39872993952

Pull #10650

github

web-flow
Merge 6369d6b54 into df8b96bee
Pull Request #10650: fix: clear transaction state when reconnect() replaces the socket

9 of 10 new or added lines in 2 files covered. (90.0%)

46 existing lines in 2 files now uncovered.

22880 of 25367 relevant lines covered (90.2%)

233.25 hits per line

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

90.32
/system/Database/BaseConnection.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;
15

16
use Closure;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Events\Events;
19
use ReflectionClass;
20
use ReflectionNamedType;
21
use ReflectionType;
22
use ReflectionUnionType;
23
use stdClass;
24
use Stringable;
25
use Throwable;
26

27
/**
28
 * @property-read list<string>                    $aliasedTables
29
 * @property-read string                          $charset
30
 * @property-read bool                            $compress
31
 * @property-read float                           $connectDuration
32
 * @property-read float                           $connectTime
33
 * @property-read string                          $database
34
 * @property-read array<string, string>           $dateFormat
35
 * @property-read string                          $DBCollat
36
 * @property-read bool                            $DBDebug
37
 * @property-read string                          $DBDriver
38
 * @property-read string                          $DBPrefix
39
 * @property-read string                          $DSN
40
 * @property-read array<string, bool|string>|bool $encrypt
41
 * @property-read list<array<string, mixed>>      $failover
42
 * @property-read string                          $hostname
43
 * @property-read Query                           $lastQuery
44
 * @property-read string                          $password
45
 * @property-read bool                            $pConnect
46
 * @property-read int|string                      $port
47
 * @property-read bool                            $pretend
48
 * @property-read string                          $queryClass
49
 * @property-read list<string>                    $reservedIdentifiers
50
 * @property-read bool                            $strictOn
51
 * @property-read string                          $subdriver
52
 * @property-read string                          $swapPre
53
 * @property-read int                             $transDepth
54
 * @property-read bool                            $transFailure
55
 * @property-read bool                            $transStatus
56
 * @property-read string                          $username
57
 *
58
 * @template TConnection
59
 * @template TResult
60
 *
61
 * @implements ConnectionInterface<TConnection, TResult>
62
 */
63
abstract class BaseConnection implements ConnectionInterface
64
{
65
    /**
66
     * Cached builtin type names per class/property.
67
     *
68
     * @var array<class-string, array<string, list<string>>>
69
     */
70
    private static array $propertyBuiltinTypesCache = [];
71

72
    /**
73
     * Data Source Name / Connect string
74
     *
75
     * @var string
76
     */
77
    protected $DSN;
78

79
    /**
80
     * Database port
81
     *
82
     * @var int|string
83
     */
84
    protected $port = '';
85

86
    /**
87
     * Hostname
88
     *
89
     * @var string
90
     */
91
    protected $hostname;
92

93
    /**
94
     * Username
95
     *
96
     * @var string
97
     */
98
    protected $username;
99

100
    /**
101
     * Password
102
     *
103
     * @var string
104
     */
105
    protected $password;
106

107
    /**
108
     * Database name
109
     *
110
     * @var string
111
     */
112
    protected $database;
113

114
    /**
115
     * Database driver
116
     *
117
     * @var string
118
     */
119
    protected $DBDriver = 'MySQLi';
120

121
    /**
122
     * Sub-driver
123
     *
124
     * @used-by CI_DB_pdo_driver
125
     *
126
     * @var string
127
     */
128
    protected $subdriver;
129

130
    /**
131
     * Table prefix
132
     *
133
     * @var string
134
     */
135
    protected $DBPrefix = '';
136

137
    /**
138
     * Persistent connection flag
139
     *
140
     * @var bool
141
     */
142
    protected $pConnect = false;
143

144
    /**
145
     * Whether to throw Exception or not when an error occurs.
146
     *
147
     * @var bool
148
     */
149
    protected $DBDebug = true;
150

151
    /**
152
     * Character set
153
     *
154
     * This value must be updated by Config\Database if the driver use it.
155
     *
156
     * @var string
157
     */
158
    protected $charset = '';
159

160
    /**
161
     * Collation
162
     *
163
     * This value must be updated by Config\Database if the driver use it.
164
     *
165
     * @var string
166
     */
167
    protected $DBCollat = '';
168

169
    /**
170
     * Swap Prefix
171
     *
172
     * @var string
173
     */
174
    protected $swapPre = '';
175

176
    /**
177
     * Encryption flag/data
178
     *
179
     * @var array<string, bool|string>|bool
180
     */
181
    protected $encrypt = false;
182

183
    /**
184
     * Compression flag
185
     *
186
     * @var bool
187
     */
188
    protected $compress = false;
189

190
    /**
191
     * Strict ON flag
192
     *
193
     * Whether we're running in strict SQL mode.
194
     *
195
     * @var bool|null
196
     *
197
     * @deprecated 4.5.0 Will move to MySQLi\Connection.
198
     */
199
    protected $strictOn;
200

201
    /**
202
     * Settings for a failover connection.
203
     *
204
     * @var list<array<string, mixed>>
205
     */
206
    protected $failover = [];
207

208
    /**
209
     * The last query object that was executed
210
     * on this connection.
211
     *
212
     * @var Query
213
     */
214
    protected $lastQuery;
215

216
    /**
217
     * Connection ID
218
     *
219
     * @var false|TConnection
220
     */
221
    public $connID = false;
222

223
    /**
224
     * Result ID
225
     *
226
     * @var false|TResult
227
     */
228
    public $resultID = false;
229

230
    /**
231
     * Protect identifiers flag
232
     *
233
     * @var bool
234
     */
235
    public $protectIdentifiers = true;
236

237
    /**
238
     * List of reserved identifiers
239
     *
240
     * Identifiers that must NOT be escaped.
241
     *
242
     * @var list<string>
243
     */
244
    protected $reservedIdentifiers = ['*'];
245

246
    /**
247
     * Identifier escape character
248
     *
249
     * @var list<string>|string
250
     */
251
    public $escapeChar = '"';
252

253
    /**
254
     * ESCAPE statement string
255
     *
256
     * @var string
257
     */
258
    public $likeEscapeStr = " ESCAPE '%s' ";
259

260
    /**
261
     * ESCAPE character
262
     *
263
     * @var string
264
     */
265
    public $likeEscapeChar = '!';
266

267
    /**
268
     * RegExp used to escape identifiers
269
     *
270
     * @var list<string>
271
     */
272
    protected $pregEscapeChar = [];
273

274
    /**
275
     * @var array<string, mixed>
276
     */
277
    public $dataCache = [];
278

279
    /**
280
     * Microtime when connection was made
281
     *
282
     * @var float
283
     */
284
    protected $connectTime = 0.0;
285

286
    /**
287
     * How long it took to establish connection.
288
     *
289
     * @var float
290
     */
291
    protected $connectDuration = 0.0;
292

293
    /**
294
     * If true, no queries will actually be
295
     * run against the database.
296
     *
297
     * @var bool
298
     */
299
    protected $pretend = false;
300

301
    /**
302
     * Transaction enabled flag
303
     *
304
     * @var bool
305
     */
306
    public $transEnabled = true;
307

308
    /**
309
     * Strict transaction mode flag
310
     *
311
     * @var bool
312
     */
313
    public $transStrict = true;
314

315
    /**
316
     * Transaction depth level
317
     *
318
     * @var int
319
     */
320
    protected $transDepth = 0;
321

322
    /**
323
     * Transaction status flag
324
     *
325
     * Used with transactions to determine if a rollback should occur.
326
     *
327
     * @var bool
328
     */
329
    protected $transStatus = true;
330

331
    /**
332
     * Transaction failure flag
333
     *
334
     * Used with transactions to determine if a transaction has failed.
335
     *
336
     * @var bool
337
     */
338
    protected $transFailure = false;
339

340
    /**
341
     * Whether to throw exceptions during transaction
342
     */
343
    protected bool $transException = false;
344

345
    /**
346
     * Array of table aliases.
347
     *
348
     * @var list<string>
349
     */
350
    protected $aliasedTables = [];
351

352
    /**
353
     * Query Class
354
     *
355
     * @var string
356
     */
357
    protected $queryClass = Query::class;
358

359
    /**
360
     * Default Date/Time formats
361
     *
362
     * @var array<string, string>
363
     */
364
    protected array $dateFormat = [
365
        'date'        => 'Y-m-d',
366
        'datetime'    => 'Y-m-d H:i:s',
367
        'datetime-ms' => 'Y-m-d H:i:s.v',
368
        'datetime-us' => 'Y-m-d H:i:s.u',
369
        'time'        => 'H:i:s',
370
    ];
371

372
    /**
373
     * @param array<string, mixed> $params
374
     */
375
    public function __construct(array $params)
376
    {
377
        if (isset($params['dateFormat'])) {
461 ✔
378
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
120 ✔
379
            unset($params['dateFormat']);
120 ✔
380
        }
381

382
        $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($params));
461 ✔
383

384
        foreach ($params as $key => $value) {
461 ✔
385
            if (property_exists($this, $key)) {
154 ✔
386
                $this->{$key} = $this->castScalarValueForTypedProperty(
154 ✔
387
                    $value,
154 ✔
388
                    $typedPropertyTypes[$key] ?? [],
154 ✔
389
                );
154 ✔
390
            }
391
        }
392

393
        $queryClass = str_replace('Connection', 'Query', static::class);
460 ✔
394

395
        if (class_exists($queryClass)) {
460 ✔
396
            $this->queryClass = $queryClass;
379 ✔
397
        }
398

399
        if ($this->failover !== []) {
460 ✔
400
            // If there is a failover database, connect now to do failover.
401
            // Otherwise, Query Builder creates SQL statement with the main database config
402
            // (DBPrefix) even when the main database is down.
403
            $this->initialize();
2 ✔
404
        }
405
    }
406

407
    /**
408
     * Some config values (especially env overrides without clear source type)
409
     * can still reach us as strings. Coerce them for typed properties to keep
410
     * strict typing compatible.
411
     *
412
     * @param list<string> $types
413
     */
414
    private function castScalarValueForTypedProperty(mixed $value, array $types): mixed
415
    {
416
        if (! is_string($value)) {
154 ✔
417
            return $value;
131 ✔
418
        }
419

420
        if ($types === [] || in_array('string', $types, true) || in_array('mixed', $types, true)) {
154 ✔
421
            return $value;
154 ✔
422
        }
423

424
        $trimmedValue = trim($value);
5 ✔
425

426
        if (in_array('null', $types, true) && strtolower($trimmedValue) === 'null') {
5 ✔
427
            return null;
1 ✔
428
        }
429

430
        if (in_array('int', $types, true) && preg_match('/^[+-]?\d+$/', $trimmedValue) === 1) {
5 ✔
431
            return (int) $trimmedValue;
2 ✔
432
        }
433

434
        if (in_array('float', $types, true) && is_numeric($trimmedValue)) {
4 ✔
435
            return (float) $trimmedValue;
×
436
        }
437

438
        if (in_array('bool', $types, true) || in_array('false', $types, true) || in_array('true', $types, true)) {
4 ✔
439
            $boolValue = filter_var($trimmedValue, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
3 ✔
440

441
            if ($boolValue !== null) {
3 ✔
442
                if (in_array('bool', $types, true)) {
3 ✔
443
                    return $boolValue;
2 ✔
444
                }
445

446
                if ($boolValue === false && in_array('false', $types, true)) {
1 ✔
447
                    return false;
1 ✔
448
                }
449

450
                if ($boolValue === true && in_array('true', $types, true)) {
1 ✔
451
                    return true;
1 ✔
452
                }
453
            }
454
        }
455

456
        return $value;
1 ✔
457
    }
458

459
    /**
460
     * @param list<string> $properties
461
     *
462
     * @return array<string, list<string>>
463
     */
464
    private function getBuiltinPropertyTypesMap(array $properties): array
465
    {
466
        $className = static::class;
461 ✔
467
        $requested = array_fill_keys($properties, true);
461 ✔
468

469
        self::$propertyBuiltinTypesCache[$className] ??= [];
461 ✔
470

471
        // Fill only the properties requested by this call that are not cached yet.
472
        $missing = array_diff_key($requested, self::$propertyBuiltinTypesCache[$className]);
461 ✔
473

474
        if ($missing !== []) {
461 ✔
475
            $reflection = new ReflectionClass($className);
29 ✔
476

477
            foreach ($reflection->getProperties() as $property) {
29 ✔
478
                $propertyName = $property->getName();
29 ✔
479

480
                if (! isset($missing[$propertyName])) {
29 ✔
481
                    continue;
29 ✔
482
                }
483

484
                $type = $property->getType();
29 ✔
485

486
                if (! $type instanceof ReflectionType) {
29 ✔
487
                    self::$propertyBuiltinTypesCache[$className][$propertyName] = [];
28 ✔
488

489
                    continue;
28 ✔
490
                }
491

492
                $namedTypes   = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
16 ✔
493
                $builtinTypes = [];
16 ✔
494

495
                foreach ($namedTypes as $namedType) {
16 ✔
496
                    if (! $namedType instanceof ReflectionNamedType || ! $namedType->isBuiltin()) {
16 ✔
497
                        continue;
×
498
                    }
499

500
                    $builtinTypes[] = $namedType->getName();
16 ✔
501
                }
502

503
                if ($type->allowsNull() && ! in_array('null', $builtinTypes, true)) {
16 ✔
504
                    $builtinTypes[] = 'null';
13 ✔
505
                }
506

507
                self::$propertyBuiltinTypesCache[$className][$propertyName] = $builtinTypes;
16 ✔
508
            }
509

510
            // Untyped or unresolved properties are cached as empty to avoid re-reflecting them.
511
            foreach (array_keys($missing) as $propertyName) {
29 ✔
512
                self::$propertyBuiltinTypesCache[$className][$propertyName] ??= [];
29 ✔
513
            }
514
        }
515

516
        $typedProperties = [];
461 ✔
517

518
        foreach ($properties as $property) {
461 ✔
519
            $typedProperties[$property] = self::$propertyBuiltinTypesCache[$className][$property] ?? [];
154 ✔
520
        }
521

522
        return $typedProperties;
461 ✔
523
    }
524

525
    /**
526
     * Initializes the database connection/settings.
527
     *
528
     * @return void
529
     *
530
     * @throws DatabaseException
531
     */
532
    public function initialize()
533
    {
534
        /* If an established connection is available, then there's
535
         * no need to connect and select the database.
536
         *
537
         * Depending on the database driver, connID can be either
538
         * boolean TRUE, a resource or an object.
539
         */
540
        if ($this->connID) {
858 ✔
541
            return;
815 ✔
542
        }
543

544
        $this->connectTime = microtime(true);
56 ✔
545
        $connectionErrors  = [];
56 ✔
546

547
        try {
548
            // Connect to the database and set the connection ID
549
            $this->connID = $this->connect($this->pConnect);
56 ✔
550
        } catch (Throwable $e) {
2 ✔
551
            $this->connID       = false;
2 ✔
552
            $connectionErrors[] = sprintf(
2 ✔
553
                'Main connection [%s]: %s',
2 ✔
554
                $this->DBDriver,
2 ✔
555
                $e->getMessage(),
2 ✔
556
            );
2 ✔
557
            log_message('error', 'Error connecting to the database: ' . $e);
2 ✔
558
        }
559

560
        // No connection resource? Check if there is a failover else throw an error
561
        if (! $this->connID) {
56 ✔
562
            // Check if there is a failover set
563
            if (is_array($this->failover) && $this->failover !== []) {
4 ✔
564
                // Go over all the failovers
565
                foreach ($this->failover as $index => $failover) {
2 ✔
566
                    $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($failover));
2 ✔
567

568
                    // Replace the current settings with those of the failover
569
                    foreach ($failover as $key => $val) {
2 ✔
570
                        if (property_exists($this, $key)) {
2 ✔
571
                            $this->{$key} = $this->castScalarValueForTypedProperty(
2 ✔
572
                                $val,
2 ✔
573
                                $typedPropertyTypes[$key] ?? [],
2 ✔
574
                            );
2 ✔
575
                        }
576
                    }
577

578
                    try {
579
                        // Try to connect
580
                        $this->connID = $this->connect($this->pConnect);
2 ✔
581
                    } catch (Throwable $e) {
1 ✔
582
                        $connectionErrors[] = sprintf(
1 ✔
583
                            'Failover #%d [%s]: %s',
1 ✔
584
                            ++$index,
1 ✔
585
                            $this->DBDriver,
1 ✔
586
                            $e->getMessage(),
1 ✔
587
                        );
1 ✔
588
                        log_message('error', 'Error connecting to the database: ' . $e);
1 ✔
589
                    }
590

591
                    // If a connection is made break the foreach loop
592
                    if ($this->connID) {
2 ✔
593
                        break;
2 ✔
594
                    }
595
                }
596
            }
597

598
            // We still don't have a connection?
599
            if (! $this->connID) {
4 ✔
600
                throw new DatabaseException(sprintf(
2 ✔
601
                    'Unable to connect to the database.%s%s',
2 ✔
602
                    PHP_EOL,
2 ✔
603
                    implode(PHP_EOL, $connectionErrors),
2 ✔
604
                ));
2 ✔
605
            }
606
        }
607

608
        $this->connectDuration = microtime(true) - $this->connectTime;
54 ✔
609
    }
610

611
    /**
612
     * Close the database connection.
613
     *
614
     * @return void
615
     */
616
    public function close()
617
    {
618
        if ($this->connID) {
8 ✔
619
            $this->_close();
7 ✔
620
            $this->connID = false;
7 ✔
621
        }
622
    }
623

624
    /**
625
     * Keep or establish the connection if no queries have been sent for
626
     * a length of time exceeding the server's idle timeout.
627
     *
628
     * A replaced socket is not in the transaction that was open on the old
629
     * one. The server has already rolled that work back. Clear the depth and
630
     * mark the transaction failed so transComplete() cannot report success.
631
     *
632
     * @return void
633
     */
634
    public function reconnect()
635
    {
636
        if ($this->ping() === false) {
4 ✔
637
            $lostTransaction = $this->transDepth > 0;
2 ✔
638

639
            $this->close();
2 ✔
640
            $this->initialize();
2 ✔
641

642
            if ($lostTransaction) {
2 ✔
643
                $this->transDepth  = 0;
1 ✔
644
                $this->transStatus = false;
1 ✔
645
            }
646
        }
647
    }
648

649
    /**
650
     * Platform dependent way method for closing the connection.
651
     *
652
     * @return void
653
     */
654
    abstract protected function _close();
655

656
    /**
657
     * Check if the connection is still alive.
658
     */
659
    public function ping(): bool
660
    {
661
        if ($this->connID === false) {
7 ✔
662
            return false;
2 ✔
663
        }
664

665
        return $this->_ping();
6 ✔
666
    }
667

668
    /**
669
     * Driver-specific ping implementation.
670
     */
671
    protected function _ping(): bool
672
    {
673
        try {
674
            $result = $this->simpleQuery('SELECT 1');
6 ✔
675

676
            return $result !== false;
5 ✔
677
        } catch (DatabaseException) {
1 ✔
678
            return false;
1 ✔
679
        }
680
    }
681

682
    /**
683
     * Create a persistent database connection.
684
     *
685
     * @return false|TConnection
686
     */
687
    public function persistentConnect()
688
    {
UNCOV
689
        return $this->connect(true);
×
690
    }
691

692
    /**
693
     * Returns the actual connection object. If both a 'read' and 'write'
694
     * connection has been specified, you can pass either term in to
695
     * get that connection. If you pass either alias in and only a single
696
     * connection is present, it must return the sole connection.
697
     *
698
     * @return false|TConnection
699
     */
700
    public function getConnection(?string $alias = null)
701
    {
702
        // @todo work with read/write connections
703
        return $this->connID;
2 ✔
704
    }
705

706
    /**
707
     * Returns the name of the current database being used.
708
     */
709
    public function getDatabase(): string
710
    {
711
        return is_string($this->database) ? $this->database : '';
785 ✔
712
    }
713

714
    /**
715
     * Set DB Prefix
716
     *
717
     * Set's the DB Prefix to something new without needing to reconnect
718
     *
719
     * @param string $prefix The prefix
720
     */
721
    public function setPrefix(string $prefix = ''): string
722
    {
723
        return $this->DBPrefix = $prefix;
14 ✔
724
    }
725

726
    /**
727
     * Returns the database prefix.
728
     */
729
    public function getPrefix(): string
730
    {
731
        return $this->DBPrefix;
13 ✔
732
    }
733

734
    /**
735
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
736
     */
737
    public function getPlatform(): string
738
    {
739
        return $this->DBDriver;
23 ✔
740
    }
741

742
    /**
743
     * Sets the Table Aliases to use. These are typically
744
     * collected during use of the Builder, and set here
745
     * so queries are built correctly.
746
     *
747
     * @param list<string> $aliases
748
     *
749
     * @return $this
750
     */
751
    public function setAliasedTables(array $aliases)
752
    {
753
        $this->aliasedTables = $aliases;
1,024 ✔
754

755
        return $this;
1,024 ✔
756
    }
757

758
    /**
759
     * Add a table alias to our list.
760
     *
761
     * @return $this
762
     */
763
    public function addTableAlias(string $alias)
764
    {
765
        if ($alias === '') {
40 ✔
766
            return $this;
6 ✔
767
        }
768

769
        if (! in_array($alias, $this->aliasedTables, true)) {
34 ✔
770
            $this->aliasedTables[] = $alias;
34 ✔
771
        }
772

773
        return $this;
34 ✔
774
    }
775

776
    /**
777
     * Executes the query against the database.
778
     *
779
     * @return false|TResult
780
     */
781
    abstract protected function execute(string $sql);
782

783
    /**
784
     * Orchestrates a query against the database. Queries must use
785
     * Database\Statement objects to store the query and build it.
786
     * This method works with the cache.
787
     *
788
     * Should automatically handle different connections for read/write
789
     * queries if needed.
790
     *
791
     * @param array<array-key, mixed>|string|null $binds
792
     *
793
     * @return BaseResult<TConnection, TResult>|bool|Query
794
     *
795
     * @todo BC set $queryClass default as null in 4.1
796
     */
797
    public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
798
    {
799
        $queryClass = $queryClass !== '' && $queryClass !== '0' ? $queryClass : $this->queryClass;
846 ✔
800

801
        if ($this->connID === false) {
846 ✔
802
            $this->initialize();
26 ✔
803
        }
804

805
        /** @var Query $query */
806
        $query = new $queryClass($this);
846 ✔
807

808
        $query->setQuery($sql, $binds, $setEscapeFlags);
846 ✔
809

810
        if ($this->swapPre !== '' && $this->DBPrefix !== '') {
846 ✔
UNCOV
811
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
812
        }
813

814
        $startTime = microtime(true);
846 ✔
815

816
        // Always save the last query so we can use
817
        // the getLastQuery() method.
818
        $this->lastQuery = $query;
846 ✔
819

820
        // If $pretend is true, then we just want to return
821
        // the actual query object here. There won't be
822
        // any results to return.
823
        if ($this->pretend) {
846 ✔
824
            $query->setDuration($startTime);
10 ✔
825

826
            return $query;
10 ✔
827
        }
828

829
        // Run the query for real
830
        try {
831
            $exception      = null;
846 ✔
832
            $this->resultID = $this->simpleQuery($query->getQuery());
846 ✔
833
        } catch (DatabaseException $exception) {
17 ✔
834
            $this->resultID = false;
17 ✔
835
        }
836

837
        if ($this->resultID === false) {
846 ✔
838
            $query->setDuration($startTime, $startTime);
36 ✔
839

840
            // This will trigger a rollback if transactions are being used
841
            $this->handleTransStatus();
36 ✔
842

843
            if (
844
                $this->DBDebug
36 ✔
845
                && (
846
                    // Not in transactions
847
                    $this->transDepth === 0
36 ✔
848
                    // In transactions, do not throw exception by default.
36 ✔
849
                    || $this->transException
36 ✔
850
                )
851
            ) {
852
                // We call this function in order to roll-back queries
853
                // if transactions are enabled. If we don't call this here
854
                // the error message will trigger an exit, causing the
855
                // transactions to remain in limbo.
856
                while ($this->transDepth !== 0) {
13 ✔
857
                    $transDepth = $this->transDepth;
2 ✔
858
                    $this->transComplete();
2 ✔
859

860
                    if ($transDepth === $this->transDepth) {
2 ✔
UNCOV
861
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
UNCOV
862
                        break;
×
863
                    }
864
                }
865

866
                // Let others do something with this query.
867
                Events::trigger('DBQuery', $query);
13 ✔
868

869
                if ($exception instanceof DatabaseException) {
13 ✔
870
                    throw new DatabaseException(
11 ✔
871
                        $exception->getMessage(),
11 ✔
872
                        $exception->getCode(),
11 ✔
873
                        $exception,
11 ✔
874
                    );
11 ✔
875
                }
876

877
                return false;
2 ✔
878
            }
879

880
            // Let others do something with this query.
881
            Events::trigger('DBQuery', $query);
23 ✔
882

883
            return false;
23 ✔
884
        }
885

886
        $query->setDuration($startTime);
846 ✔
887

888
        // Let others do something with this query
889
        Events::trigger('DBQuery', $query);
846 ✔
890

891
        // resultID is not false, so it must be successful
892
        if ($this->isWriteType($sql)) {
846 ✔
893
            return true;
814 ✔
894
        }
895

896
        // query is not write-type, so it must be read-type query; return QueryResult
897
        $resultClass = str_replace('Connection', 'Result', static::class);
846 ✔
898

899
        return new $resultClass($this->connID, $this->resultID);
846 ✔
900
    }
901

902
    /**
903
     * Performs a basic query against the database. No binding or caching
904
     * is performed, nor are transactions handled. Simply takes a raw
905
     * query string and returns the database-specific result id.
906
     *
907
     * @return false|TResult
908
     */
909
    public function simpleQuery(string $sql)
910
    {
911
        if ($this->connID === false) {
856 ✔
912
            $this->initialize();
9 ✔
913
        }
914

915
        return $this->execute($sql);
856 ✔
916
    }
917

918
    /**
919
     * Disable Transactions
920
     *
921
     * This permits transactions to be disabled at run-time.
922
     *
923
     * @return void
924
     */
925
    public function transOff()
926
    {
UNCOV
927
        $this->transEnabled = false;
×
928
    }
929

930
    /**
931
     * Enable/disable Transaction Strict Mode
932
     *
933
     * When strict mode is enabled, if you are running multiple groups of
934
     * transactions, if one group fails all subsequent groups will be
935
     * rolled back.
936
     *
937
     * If strict mode is disabled, each group is treated autonomously,
938
     * meaning a failure of one group will not affect any others
939
     *
940
     * @param bool $mode = true
941
     *
942
     * @return $this
943
     */
944
    public function transStrict(bool $mode = true)
945
    {
946
        $this->transStrict = $mode;
4 ✔
947

948
        return $this;
4 ✔
949
    }
950

951
    /**
952
     * Start Transaction
953
     */
954
    public function transStart(bool $testMode = false): bool
955
    {
956
        if (! $this->transEnabled) {
52 ✔
UNCOV
957
            return false;
×
958
        }
959

960
        return $this->transBegin($testMode);
52 ✔
961
    }
962

963
    /**
964
     * If set to true, exceptions are thrown during transactions.
965
     *
966
     * @return $this
967
     */
968
    public function transException(bool $transException)
969
    {
970
        $this->transException = $transException;
3 ✔
971

972
        return $this;
3 ✔
973
    }
974

975
    /**
976
     * Complete Transaction
977
     */
978
    public function transComplete(): bool
979
    {
980
        if (! $this->transEnabled) {
52 ✔
UNCOV
981
            return false;
×
982
        }
983

984
        // The query() function will set this flag to FALSE in the event that a query failed
985
        if ($this->transStatus === false || $this->transFailure === true) {
52 ✔
986
            $this->transRollback();
17 ✔
987

988
            // If we are NOT running in strict mode, we will reset
989
            // the _trans_status flag so that subsequent groups of
990
            // transactions will be permitted.
991
            if ($this->transStrict === false) {
17 ✔
992
                $this->transStatus = true;
4 ✔
993
            }
994

995
            return false;
17 ✔
996
        }
997

998
        return $this->transCommit();
40 ✔
999
    }
1000

1001
    /**
1002
     * Lets you retrieve the transaction flag to determine if it has failed
1003
     */
1004
    public function transStatus(): bool
1005
    {
1006
        return $this->transStatus;
17 ✔
1007
    }
1008

1009
    /**
1010
     * Begin Transaction
1011
     */
1012
    public function transBegin(bool $testMode = false): bool
1013
    {
1014
        if (! $this->transEnabled) {
55 ✔
UNCOV
1015
            return false;
×
1016
        }
1017

1018
        // When transactions are nested we only begin/commit/rollback the outermost ones
1019
        if ($this->transDepth > 0) {
55 ✔
UNCOV
1020
            $this->transDepth++;
×
1021

UNCOV
1022
            return true;
×
1023
        }
1024

1025
        if ($this->connID === false) {
55 ✔
UNCOV
1026
            $this->initialize();
×
1027
        }
1028

1029
        // Reset the transaction failure flag.
1030
        // If the $testMode flag is set to TRUE transactions will be rolled back
1031
        // even if the queries produce a successful result.
1032
        $this->transFailure = $testMode;
55 ✔
1033

1034
        if ($this->_transBegin()) {
55 ✔
1035
            $this->transDepth++;
55 ✔
1036

1037
            return true;
55 ✔
1038
        }
1039

UNCOV
1040
        return false;
×
1041
    }
1042

1043
    /**
1044
     * Commit Transaction
1045
     */
1046
    public function transCommit(): bool
1047
    {
1048
        if (! $this->transEnabled || $this->transDepth === 0) {
40 ✔
UNCOV
1049
            return false;
×
1050
        }
1051

1052
        // When transactions are nested we only begin/commit/rollback the outermost ones
1053
        if ($this->transDepth > 1 || $this->_transCommit()) {
40 ✔
1054
            $this->transDepth--;
40 ✔
1055

1056
            return true;
40 ✔
1057
        }
1058

UNCOV
1059
        return false;
×
1060
    }
1061

1062
    /**
1063
     * Rollback Transaction
1064
     */
1065
    public function transRollback(): bool
1066
    {
1067
        if (! $this->transEnabled || $this->transDepth === 0) {
20 ✔
1068
            return false;
1 ✔
1069
        }
1070

1071
        // When transactions are nested we only begin/commit/rollback the outermost ones
1072
        if ($this->transDepth > 1 || $this->_transRollback()) {
19 ✔
1073
            $this->transDepth--;
19 ✔
1074

1075
            return true;
19 ✔
1076
        }
1077

UNCOV
1078
        return false;
×
1079
    }
1080

1081
    /**
1082
     * Reset transaction status - to restart transactions after strict mode failure
1083
     */
1084
    public function resetTransStatus(): static
1085
    {
1086
        $this->transStatus = true;
4 ✔
1087

1088
        return $this;
4 ✔
1089
    }
1090

1091
    /**
1092
     * Handle transaction status when a query fails
1093
     *
1094
     * @internal This method is for internal database component use only
1095
     */
1096
    public function handleTransStatus(): void
1097
    {
1098
        if ($this->transDepth !== 0) {
40 ✔
1099
            $this->transStatus = false;
18 ✔
1100
        }
1101
    }
1102

1103
    /**
1104
     * Begin Transaction
1105
     */
1106
    abstract protected function _transBegin(): bool;
1107

1108
    /**
1109
     * Commit Transaction
1110
     */
1111
    abstract protected function _transCommit(): bool;
1112

1113
    /**
1114
     * Rollback Transaction
1115
     */
1116
    abstract protected function _transRollback(): bool;
1117

1118
    /**
1119
     * Returns a non-shared new instance of the query builder for this connection.
1120
     *
1121
     * @param array<array-key, string>|string|TableName $tableName
1122
     *
1123
     * @return BaseBuilder
1124
     *
1125
     * @throws DatabaseException
1126
     */
1127
    public function table($tableName)
1128
    {
1129
        if (in_array($tableName, ['', '0', []], true)) {
959 ✔
UNCOV
1130
            throw new DatabaseException('You must set the database table to be used with your query.');
×
1131
        }
1132

1133
        $className = str_replace('Connection', 'Builder', static::class);
959 ✔
1134

1135
        return new $className($tableName, $this);
959 ✔
1136
    }
1137

1138
    /**
1139
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
1140
     */
1141
    public function newQuery(): BaseBuilder
1142
    {
1143
        // save table aliases
1144
        $tempAliases         = $this->aliasedTables;
14 ✔
1145
        $builder             = $this->table(',')->from([], true);
14 ✔
1146
        $this->aliasedTables = $tempAliases;
14 ✔
1147

1148
        return $builder;
14 ✔
1149
    }
1150

1151
    /**
1152
     * Creates a prepared statement with the database that can then
1153
     * be used to execute multiple statements against. Within the
1154
     * closure, you would build the query in any normal way, though
1155
     * the Query Builder is the expected manner.
1156
     *
1157
     * Example:
1158
     *    $stmt = $db->prepare(function($db)
1159
     *           {
1160
     *             return $db->table('users')
1161
     *                   ->where('id', 1)
1162
     *                     ->get();
1163
     *           })
1164
     *
1165
     * @param Closure(BaseConnection): mixed $func
1166
     * @param array<string, mixed>           $options
1167
     *
1168
     * @return BasePreparedQuery|null
1169
     */
1170
    public function prepare(Closure $func, array $options = [])
1171
    {
1172
        if ($this->connID === false) {
16 ✔
UNCOV
1173
            $this->initialize();
×
1174
        }
1175

1176
        $this->pretend();
16 ✔
1177

1178
        $sql = $func($this);
16 ✔
1179

1180
        $this->pretend(false);
16 ✔
1181

1182
        if ($sql instanceof QueryInterface) {
16 ✔
1183
            $sql = $sql->getOriginalQuery();
16 ✔
1184
        }
1185

1186
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
16 ✔
1187
        /** @var BasePreparedQuery $class */
1188
        $class = new $class($this);
16 ✔
1189

1190
        return $class->prepare($sql, $options);
16 ✔
1191
    }
1192

1193
    /**
1194
     * Returns the last query's statement object.
1195
     *
1196
     * @return Query
1197
     */
1198
    public function getLastQuery()
1199
    {
1200
        return $this->lastQuery;
14 ✔
1201
    }
1202

1203
    /**
1204
     * Returns a string representation of the last query's statement object.
1205
     */
1206
    public function showLastQuery(): string
1207
    {
UNCOV
1208
        return (string) $this->lastQuery;
×
1209
    }
1210

1211
    /**
1212
     * Returns the time we started to connect to this database in
1213
     * seconds with microseconds.
1214
     *
1215
     * Used by the Debug Toolbar's timeline.
1216
     */
1217
    public function getConnectStart(): ?float
1218
    {
1219
        return $this->connectTime;
1 ✔
1220
    }
1221

1222
    /**
1223
     * Returns the number of seconds with microseconds that it took
1224
     * to connect to the database.
1225
     *
1226
     * Used by the Debug Toolbar's timeline.
1227
     */
1228
    public function getConnectDuration(int $decimals = 6): string
1229
    {
1230
        return number_format($this->connectDuration, $decimals);
2 ✔
1231
    }
1232

1233
    /**
1234
     * Protect Identifiers
1235
     *
1236
     * This function is used extensively by the Query Builder class, and by
1237
     * a couple functions in this class.
1238
     * It takes a column or table name (optionally with an alias) and inserts
1239
     * the table prefix onto it. Some logic is necessary in order to deal with
1240
     * column names that include the path. Consider a query like this:
1241
     *
1242
     * SELECT hostname.database.table.column AS c FROM hostname.database.table
1243
     *
1244
     * Or a query with aliasing:
1245
     *
1246
     * SELECT m.member_id, m.member_name FROM members AS m
1247
     *
1248
     * Since the column name can include up to four segments (host, DB, table, column)
1249
     * or also have an alias prefix, we need to do a bit of work to figure this out and
1250
     * insert the table prefix (if it exists) in the proper position, and escape only
1251
     * the correct identifiers.
1252
     *
1253
     * @param array<array-key, mixed>|int|string|TableName $item
1254
     * @param bool                                         $prefixSingle       Prefix a table name with no segments?
1255
     * @param bool                                         $protectIdentifiers Protect table or column names?
1256
     * @param bool                                         $fieldExists        Supplied $item contains a column name?
1257
     *
1258
     * @return ($item is array ? array<array-key, mixed> : string)
1259
     */
1260
    public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
1261
    {
1262
        if (! is_bool($protectIdentifiers)) {
1,140 ✔
1263
            $protectIdentifiers = $this->protectIdentifiers;
1,102 ✔
1264
        }
1265

1266
        if (is_array($item)) {
1,140 ✔
1267
            $escapedArray = [];
1 ✔
1268

1269
            foreach ($item as $k => $v) {
1 ✔
1270
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1 ✔
1271
            }
1272

1273
            return $escapedArray;
1 ✔
1274
        }
1275

1276
        if ($item instanceof TableName) {
1,140 ✔
1277
            /** @psalm-suppress NoValue I don't know why ERROR. */
1278
            return $this->escapeTableName($item);
6 ✔
1279
        }
1280

1281
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1282
        $item = (string) $item;
1,140 ✔
1283

1284
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1285
        // If a parenthesis is found we know that we do not need to
1286
        // escape the data or add a prefix. There's probably a more graceful
1287
        // way to deal with this, but I'm not thinking of it
1288
        //
1289
        // Added exception for single quotes as well, we don't want to alter
1290
        // literal strings.
1291
        if (strcspn($item, "()'") !== strlen($item)) {
1,140 ✔
1292
            /** @psalm-suppress NoValue I don't know why ERROR. */
1293
            return $item;
814 ✔
1294
        }
1295

1296
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1297
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
1,129 ✔
1298
            /** @psalm-suppress NoValue I don't know why ERROR. */
1299
            return $item;
101 ✔
1300
        }
1301

1302
        // Convert tabs or multiple spaces into single spaces
1303
        /** @psalm-suppress NoValue I don't know why ERROR. */
1304
        $item = preg_replace('/\s+/', ' ', trim($item));
1,128 ✔
1305

1306
        // If the item has an alias declaration we remove it and set it aside.
1307
        // Note: strripos() is used in order to support spaces in table names
1308
        if ($offset = strripos($item, ' AS ')) {
1,128 ✔
1309
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11 ✔
1310
            $item  = substr($item, 0, $offset);
11 ✔
1311
        } elseif ($offset = strrpos($item, ' ')) {
1,123 ✔
1312
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
12 ✔
1313
            $item  = substr($item, 0, $offset);
12 ✔
1314
        } else {
1315
            $alias = '';
1,117 ✔
1316
        }
1317

1318
        // Break the string apart if it contains periods, then insert the table prefix
1319
        // in the correct location, assuming the period doesn't indicate that we're dealing
1320
        // with an alias. While we're at it, we will escape the components
1321
        if (str_contains($item, '.')) {
1,128 ✔
1322
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
145 ✔
1323
        }
1324

1325
        // In some cases, especially 'from', we end up running through
1326
        // protect_identifiers twice. This algorithm won't work when
1327
        // it contains the escapeChar so strip it out.
1328
        $item = $this->trimEscapeChar($item);
1,115 ✔
1329

1330
        // Is there a table prefix? If not, no need to insert it
1331
        if ($this->DBPrefix !== '') {
1,115 ✔
1332
            // Verify table prefix and replace if necessary
1333
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
855 ✔
UNCOV
1334
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1335
            }
1336
            // Do we prefix an item with no segments?
1337
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
855 ✔
1338
                $item = $this->DBPrefix . $item;
848 ✔
1339
            }
1340
        }
1341

1342
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
1,115 ✔
1343
            $item = $this->escapeIdentifiers($item);
1,113 ✔
1344
        }
1345

1346
        return $item . $alias;
1,115 ✔
1347
    }
1348

1349
    private function trimEscapeChar(string $item): string
1350
    {
1351
        return trim($item, is_array($this->escapeChar) ? implode('', $this->escapeChar) : $this->escapeChar);
1,128 ✔
1352
    }
1353

1354
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1355
    {
1356
        $parts = array_map($this->trimEscapeChar(...), explode('.', $item));
145 ✔
1357
        $item  = implode('.', $parts);
145 ✔
1358

1359
        // Does the first segment of the exploded item match
1360
        // one of the aliases previously identified? If so,
1361
        // we have nothing more to do other than escape the item
1362
        //
1363
        // $aliasedTables is empty when QB isn't enabled.
1364
        if ($this->aliasedTables !== [] && in_array($parts[0], $this->aliasedTables, true)) {
145 ✔
1365
            if ($protectIdentifiers) {
10 ✔
1366
                foreach ($parts as $key => $val) {
10 ✔
1367
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
10 ✔
1368
                        $parts[$key] = $this->escapeIdentifiers($val);
10 ✔
1369
                    }
1370
                }
1371

1372
                $item = implode('.', $parts);
10 ✔
1373
            }
1374

1375
            return $item . $alias;
10 ✔
1376
        }
1377

1378
        // Is there a table prefix defined in the config file? If not, no need to do anything
1379
        if ($this->DBPrefix !== '') {
139 ✔
1380
            // We now add the table prefix based on some logic.
1381
            // Do we have 4 segments (hostname.database.table.column)?
1382
            // If so, we add the table prefix to the column name in the 3rd segment.
1383
            if (isset($parts[3])) {
127 ✔
UNCOV
1384
                $i = 2;
×
1385
            }
1386
            // Do we have 3 segments (database.table.column)?
1387
            // If so, we add the table prefix to the column name in 2nd position
1388
            elseif (isset($parts[2])) {
127 ✔
UNCOV
1389
                $i = 1;
×
1390
            }
1391
            // Do we have 2 segments (table.column)?
1392
            // If so, we add the table prefix to the column name in 1st segment
1393
            else {
1394
                $i = 0;
127 ✔
1395
            }
1396

1397
            // This flag is set when the supplied $item does not contain a field name.
1398
            // This can happen when this function is being called from a JOIN.
1399
            if ($fieldExists === false) {
127 ✔
1400
                $i++;
4 ✔
1401
            }
1402

1403
            // Verify table prefix and replace if necessary
1404
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
127 ✔
UNCOV
1405
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1406
            }
1407
            // We only add the table prefix if it does not already exist
1408
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
127 ✔
1409
                $parts[$i] = $this->DBPrefix . $parts[$i];
125 ✔
1410
            }
1411

1412
            // Put the parts back together
1413
            $item = implode('.', $parts);
127 ✔
1414
        }
1415

1416
        if ($protectIdentifiers) {
139 ✔
1417
            $item = $this->escapeIdentifiers($item);
138 ✔
1418
        }
1419

1420
        return $item . $alias;
139 ✔
1421
    }
1422

1423
    /**
1424
     * Escape the SQL Identifier
1425
     *
1426
     * This function escapes single identifier.
1427
     *
1428
     * @param non-empty-string|TableName $item
1429
     */
1430
    public function escapeIdentifier($item): string
1431
    {
1432
        if ($item === '') {
739 ✔
UNCOV
1433
            return '';
×
1434
        }
1435

1436
        if ($item instanceof TableName) {
739 ✔
1437
            return $this->escapeTableName($item);
7 ✔
1438
        }
1439

1440
        return $this->escapeChar
739 ✔
1441
            . str_replace(
739 ✔
1442
                $this->escapeChar,
739 ✔
1443
                $this->escapeChar . $this->escapeChar,
739 ✔
1444
                $item,
739 ✔
1445
            )
739 ✔
1446
            . $this->escapeChar;
739 ✔
1447
    }
1448

1449
    /**
1450
     * Returns escaped table name with alias.
1451
     */
1452
    private function escapeTableName(TableName $tableName): string
1453
    {
1454
        $alias = $tableName->getAlias();
7 ✔
1455

1456
        return $this->escapeIdentifier($tableName->getActualTableName())
7 ✔
1457
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
7 ✔
1458
    }
1459

1460
    /**
1461
     * Escape the SQL Identifiers
1462
     *
1463
     * This function escapes column and table names
1464
     *
1465
     * @param array<array-key, mixed>|string $item
1466
     *
1467
     * @return ($item is array ? array<array-key, mixed> : string)
1468
     */
1469
    public function escapeIdentifiers($item)
1470
    {
1471
        if ($this->escapeChar === '' || in_array($item, ['', '0', []], true) || in_array($item, $this->reservedIdentifiers, true)) {
1,144 ✔
1472
            return $item;
5 ✔
1473
        }
1474

1475
        if (is_array($item)) {
1,143 ✔
1476
            foreach ($item as $key => $value) {
758 ✔
1477
                $item[$key] = $this->escapeIdentifiers($value);
758 ✔
1478
            }
1479

1480
            return $item;
758 ✔
1481
        }
1482

1483
        // Avoid breaking functions and literal values inside queries
1484
        if (ctype_digit($item)
1,143 ✔
1485
            || $item[0] === "'"
1,142 ✔
1486
            || ($this->escapeChar !== '"' && $item[0] === '"')
1,142 ✔
1487
            || str_contains($item, '(')) {
1,143 ✔
1488
            return $item;
48 ✔
1489
        }
1490

1491
        if ($this->pregEscapeChar === []) {
1,142 ✔
1492
            if (is_array($this->escapeChar)) {
324 ✔
1493
                $this->pregEscapeChar = [
1 ✔
1494
                    preg_quote($this->escapeChar[0], '/'),
1 ✔
1495
                    preg_quote($this->escapeChar[1], '/'),
1 ✔
1496
                    $this->escapeChar[0],
1 ✔
1497
                    $this->escapeChar[1],
1 ✔
1498
                ];
1 ✔
1499
            } else {
1500
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
323 ✔
1501
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
323 ✔
1502
            }
1503
        }
1504

1505
        foreach ($this->reservedIdentifiers as $id) {
1,142 ✔
1506
            /** @psalm-suppress NoValue I don't know why ERROR. */
1507
            if (str_contains($item, '.' . $id)) {
1,142 ✔
1508
                return preg_replace(
3 ✔
1509
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3 ✔
1510
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3 ✔
1511
                    $item,
3 ✔
1512
                );
3 ✔
1513
            }
1514
        }
1515

1516
        /** @psalm-suppress NoValue I don't know why ERROR. */
1517
        return preg_replace(
1,140 ✔
1518
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
1,140 ✔
1519
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
1,140 ✔
1520
            $item,
1,140 ✔
1521
        );
1,140 ✔
1522
    }
1523

1524
    /**
1525
     * Prepends a database prefix if one exists in configuration
1526
     *
1527
     * @throws DatabaseException
1528
     */
1529
    public function prefixTable(string $table = ''): string
1530
    {
1531
        if ($table === '') {
5 ✔
UNCOV
1532
            throw new DatabaseException('A table name is required for that operation.');
×
1533
        }
1534

1535
        return $this->DBPrefix . $table;
5 ✔
1536
    }
1537

1538
    /**
1539
     * Returns the total number of rows affected by this query.
1540
     */
1541
    abstract public function affectedRows(): int;
1542

1543
    /**
1544
     * "Smart" Escape String
1545
     *
1546
     * Escapes data based on type.
1547
     * Sets boolean and null types
1548
     *
1549
     * @param mixed $str
1550
     *
1551
     * @return ($str is array ? array<array-key, mixed> : float|int|string)
1552
     */
1553
    public function escape($str)
1554
    {
1555
        if (is_array($str)) {
960 ✔
1556
            return array_map($this->escape(...), $str);
771 ✔
1557
        }
1558

1559
        if ($str instanceof Stringable) {
960 ✔
1560
            if ($str instanceof RawSql) {
15 ✔
1561
                return $str->__toString();
14 ✔
1562
            }
1563

1564
            $str = (string) $str;
1 ✔
1565
        }
1566

1567
        if (is_string($str)) {
957 ✔
1568
            return "'" . $this->escapeString($str) . "'";
900 ✔
1569
        }
1570

1571
        if (is_bool($str)) {
873 ✔
1572
            return ($str === false) ? 0 : 1;
8 ✔
1573
        }
1574

1575
        return $str ?? 'NULL';
871 ✔
1576
    }
1577

1578
    /**
1579
     * Escape String
1580
     *
1581
     * @param list<string|Stringable>|string|Stringable $str  Input string
1582
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1583
     *
1584
     * @return list<string>|string
1585
     */
1586
    public function escapeString($str, bool $like = false)
1587
    {
1588
        if (is_array($str)) {
900 ✔
UNCOV
1589
            foreach ($str as $key => $val) {
×
UNCOV
1590
                $str[$key] = $this->escapeString($val, $like);
×
1591
            }
1592

UNCOV
1593
            return $str;
×
1594
        }
1595

1596
        if ($str instanceof Stringable) {
900 ✔
1597
            if ($str instanceof RawSql) {
2 ✔
UNCOV
1598
                return $str->__toString();
×
1599
            }
1600

1601
            $str = (string) $str;
2 ✔
1602
        }
1603

1604
        $str = $this->_escapeString($str);
900 ✔
1605

1606
        // escape LIKE condition wildcards
1607
        if ($like) {
900 ✔
1608
            return str_replace(
2 ✔
1609
                [
2 ✔
1610
                    $this->likeEscapeChar,
2 ✔
1611
                    '%',
2 ✔
1612
                    '_',
2 ✔
1613
                ],
2 ✔
1614
                [
2 ✔
1615
                    $this->likeEscapeChar . $this->likeEscapeChar,
2 ✔
1616
                    $this->likeEscapeChar . '%',
2 ✔
1617
                    $this->likeEscapeChar . '_',
2 ✔
1618
                ],
2 ✔
1619
                $str,
2 ✔
1620
            );
2 ✔
1621
        }
1622

1623
        return $str;
900 ✔
1624
    }
1625

1626
    /**
1627
     * Escape LIKE String
1628
     *
1629
     * Calls the individual driver for platform
1630
     * specific escaping for LIKE conditions
1631
     *
1632
     * @param list<string|Stringable>|string|Stringable $str
1633
     *
1634
     * @return list<string>|string
1635
     */
1636
    public function escapeLikeString($str)
1637
    {
1638
        return $this->escapeString($str, true);
2 ✔
1639
    }
1640

1641
    /**
1642
     * Platform independent string escape.
1643
     *
1644
     * Will likely be overridden in child classes.
1645
     */
1646
    protected function _escapeString(string $str): string
1647
    {
1648
        return str_replace("'", "''", remove_invisible_characters($str, false));
849 ✔
1649
    }
1650

1651
    /**
1652
     * This function enables you to call PHP database functions that are not natively included
1653
     * in CodeIgniter, in a platform independent manner.
1654
     *
1655
     * @param mixed ...$params
1656
     *
1657
     * @throws DatabaseException
1658
     */
1659
    public function callFunction(string $functionName, ...$params): bool
1660
    {
1661
        $driver = $this->getDriverFunctionPrefix();
2 ✔
1662

1663
        if (! str_starts_with($functionName, $driver)) {
2 ✔
1664
            $functionName = $driver . $functionName;
1 ✔
1665
        }
1666

1667
        if (! function_exists($functionName)) {
2 ✔
UNCOV
1668
            if ($this->DBDebug) {
×
UNCOV
1669
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1670
            }
1671

UNCOV
1672
            return false;
×
1673
        }
1674

1675
        return $functionName(...$params);
2 ✔
1676
    }
1677

1678
    /**
1679
     * Get the prefix of the function to access the DB.
1680
     */
1681
    protected function getDriverFunctionPrefix(): string
1682
    {
UNCOV
1683
        return strtolower($this->DBDriver) . '_';
×
1684
    }
1685

1686
    // --------------------------------------------------------------------
1687
    // META Methods
1688
    // --------------------------------------------------------------------
1689

1690
    /**
1691
     * Returns an array of table names
1692
     *
1693
     * @return false|list<string>
1694
     *
1695
     * @throws DatabaseException
1696
     */
1697
    public function listTables(bool $constrainByPrefix = false)
1698
    {
1699
        if (($this->dataCache['table_names'] ?? []) !== []) {
799 ✔
1700
            $tables = $constrainByPrefix
792 ✔
1701
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2 ✔
1702
                : $this->dataCache['table_names'];
792 ✔
1703

1704
            return array_values($tables);
792 ✔
1705
        }
1706

1707
        $sql = $this->_listTables($constrainByPrefix);
67 ✔
1708

1709
        if ($sql === false) {
67 ✔
UNCOV
1710
            if ($this->DBDebug) {
×
UNCOV
1711
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1712
            }
1713

UNCOV
1714
            return false;
×
1715
        }
1716

1717
        $this->dataCache['table_names'] = [];
67 ✔
1718

1719
        $query = $this->query($sql);
67 ✔
1720

1721
        foreach ($query->getResultArray() as $row) {
67 ✔
1722
            /** @var string $table */
1723
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
64 ✔
1724

1725
            $this->dataCache['table_names'][] = $table;
64 ✔
1726
        }
1727

1728
        return $this->dataCache['table_names'];
67 ✔
1729
    }
1730

1731
    /**
1732
     * Determine if a particular table exists
1733
     *
1734
     * @param bool $cached Whether to use data cache
1735
     */
1736
    public function tableExists(string $tableName, bool $cached = true): bool
1737
    {
1738
        if ($cached) {
800 ✔
1739
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
799 ✔
1740
        }
1741

1742
        if (false === ($sql = $this->_listTables(false, $tableName))) {
752 ✔
UNCOV
1743
            if ($this->DBDebug) {
×
UNCOV
1744
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1745
            }
1746

UNCOV
1747
            return false;
×
1748
        }
1749

1750
        $tableExists = $this->query($sql)->getResultArray() !== [];
752 ✔
1751

1752
        // if cache has been built already
1753
        if (($this->dataCache['table_names'] ?? []) !== []) {
752 ✔
1754
            $key = array_search(
748 ✔
1755
                strtolower($tableName),
748 ✔
1756
                array_map(strtolower(...), $this->dataCache['table_names']),
748 ✔
1757
                true,
748 ✔
1758
            );
748 ✔
1759

1760
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1761
            // OR if table does exist but is not found in cache
1762
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
748 ✔
1763
                $this->resetDataCache();
1 ✔
1764
            }
1765
        }
1766

1767
        return $tableExists;
752 ✔
1768
    }
1769

1770
    /**
1771
     * Fetch Field Names
1772
     *
1773
     * @param string|TableName $tableName
1774
     *
1775
     * @return false|list<string>
1776
     *
1777
     * @throws DatabaseException
1778
     */
1779
    public function getFieldNames($tableName)
1780
    {
1781
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
12 ✔
1782

1783
        // Is there a cached result?
1784
        if (isset($this->dataCache['field_names'][$table])) {
12 ✔
1785
            return $this->dataCache['field_names'][$table];
3 ✔
1786
        }
1787

1788
        if ($this->connID === false) {
12 ✔
UNCOV
1789
            $this->initialize();
×
1790
        }
1791

1792
        if (false === ($sql = $this->_listColumns($tableName))) {
12 ✔
UNCOV
1793
            if ($this->DBDebug) {
×
UNCOV
1794
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1795
            }
1796

UNCOV
1797
            return false;
×
1798
        }
1799

1800
        $query = $this->query($sql);
12 ✔
1801

1802
        $this->dataCache['field_names'][$table] = [];
12 ✔
1803

1804
        foreach ($query->getResultArray() as $row) {
12 ✔
1805
            // Do we know from where to get the column's name?
1806
            if (! isset($key)) {
12 ✔
1807
                if (isset($row['column_name'])) {
12 ✔
1808
                    $key = 'column_name';
12 ✔
1809
                } elseif (isset($row['COLUMN_NAME'])) {
12 ✔
1810
                    $key = 'COLUMN_NAME';
12 ✔
1811
                } else {
1812
                    // We have no other choice but to just get the first element's key.
1813
                    $key = key($row);
12 ✔
1814
                }
1815
            }
1816

1817
            $this->dataCache['field_names'][$table][] = $row[$key];
12 ✔
1818
        }
1819

1820
        return $this->dataCache['field_names'][$table];
12 ✔
1821
    }
1822

1823
    /**
1824
     * Determine if a particular field exists
1825
     */
1826
    public function fieldExists(string $fieldName, string $tableName): bool
1827
    {
1828
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8 ✔
1829
    }
1830

1831
    /**
1832
     * Returns an object with field data
1833
     *
1834
     * @return list<stdClass>
1835
     */
1836
    public function getFieldData(string $table)
1837
    {
1838
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
158 ✔
1839
    }
1840

1841
    /**
1842
     * Returns an object with key data
1843
     *
1844
     * @return array<string, stdClass>
1845
     */
1846
    public function getIndexData(string $table)
1847
    {
1848
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
161 ✔
1849
    }
1850

1851
    /**
1852
     * Returns an object with foreign key data
1853
     *
1854
     * @return array<string, stdClass>
1855
     */
1856
    public function getForeignKeyData(string $table)
1857
    {
1858
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
38 ✔
1859
    }
1860

1861
    /**
1862
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1863
     *
1864
     * @param array<array-key, array{
1865
     *     constraint_name: string|null,
1866
     *     table_name: string,
1867
     *     column_name: list<string>,
1868
     *     foreign_table_name: string,
1869
     *     foreign_column_name: list<string>,
1870
     *     on_delete: string,
1871
     *     on_update: string|null,
1872
     *     match: string|null,
1873
     * }> $data
1874
     *
1875
     * @return array<string, stdClass>
1876
     */
1877
    protected function foreignKeyDataToObjects(array $data)
1878
    {
1879
        $retVal = [];
38 ✔
1880

1881
        foreach ($data as $row) {
38 ✔
1882
            $name = $row['constraint_name'];
13 ✔
1883

1884
            $name ??= $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
13 ✔
1885

1886
            $obj                      = new stdClass();
13 ✔
1887
            $obj->constraint_name     = $name;
13 ✔
1888
            $obj->table_name          = $row['table_name'];
13 ✔
1889
            $obj->column_name         = $row['column_name'];
13 ✔
1890
            $obj->foreign_table_name  = $row['foreign_table_name'];
13 ✔
1891
            $obj->foreign_column_name = $row['foreign_column_name'];
13 ✔
1892
            $obj->on_delete           = $row['on_delete'];
13 ✔
1893
            $obj->on_update           = $row['on_update'];
13 ✔
1894
            $obj->match               = $row['match'];
13 ✔
1895

1896
            $retVal[$name] = $obj;
13 ✔
1897
        }
1898

1899
        return $retVal;
38 ✔
1900
    }
1901

1902
    /**
1903
     * Disables foreign key checks temporarily.
1904
     *
1905
     * @return bool
1906
     */
1907
    public function disableForeignKeyChecks()
1908
    {
1909
        $sql = $this->_disableForeignKeyChecks();
767 ✔
1910

1911
        if ($sql === '') {
767 ✔
1912
            // The feature is not supported.
UNCOV
1913
            return false;
×
1914
        }
1915

1916
        return $this->query($sql);
767 ✔
1917
    }
1918

1919
    /**
1920
     * Enables foreign key checks temporarily.
1921
     *
1922
     * @return bool
1923
     */
1924
    public function enableForeignKeyChecks()
1925
    {
1926
        $sql = $this->_enableForeignKeyChecks();
842 ✔
1927

1928
        if ($sql === '') {
842 ✔
1929
            // The feature is not supported.
1930
            return false;
×
1931
        }
1932

1933
        return $this->query($sql);
842 ✔
1934
    }
1935

1936
    /**
1937
     * Allows the engine to be set into a mode where queries are not
1938
     * actually executed, but they are still generated, timed, etc.
1939
     *
1940
     * This is primarily used by the prepared query functionality.
1941
     *
1942
     * @return $this
1943
     */
1944
    public function pretend(bool $pretend = true)
1945
    {
1946
        $this->pretend = $pretend;
17 ✔
1947

1948
        return $this;
17 ✔
1949
    }
1950

1951
    /**
1952
     * Empties our data cache. Especially helpful during testing.
1953
     *
1954
     * @return $this
1955
     */
1956
    public function resetDataCache()
1957
    {
1958
        $this->dataCache = [];
47 ✔
1959

1960
        return $this;
47 ✔
1961
    }
1962

1963
    /**
1964
     * Determines if the statement is a write-type query or not.
1965
     *
1966
     * @param string $sql
1967
     */
1968
    public function isWriteType($sql): bool
1969
    {
1970
        return (bool) preg_match('/^\s*(WITH\s.+(\s|[)]))?"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s(?!.*\sRETURNING\s)/is', $sql);
873 ✔
1971
    }
1972

1973
    /**
1974
     * Returns the last error code and message.
1975
     *
1976
     * Must return an array with keys 'code' and 'message':
1977
     *
1978
     * @return array{code: int|string|null, message: string|null}
1979
     */
1980
    abstract public function error(): array;
1981

1982
    /**
1983
     * Insert ID
1984
     *
1985
     * @return int|string
1986
     */
1987
    abstract public function insertID();
1988

1989
    /**
1990
     * Generates the SQL for listing tables in a platform-dependent manner.
1991
     *
1992
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1993
     *
1994
     * @return false|string
1995
     */
1996
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1997

1998
    /**
1999
     * Generates a platform-specific query string so that the column names can be fetched.
2000
     *
2001
     * @param string|TableName $table
2002
     *
2003
     * @return false|string
2004
     */
2005
    abstract protected function _listColumns($table = '');
2006

2007
    /**
2008
     * Platform-specific field data information.
2009
     *
2010
     * @see getFieldData()
2011
     *
2012
     * @return list<stdClass>
2013
     */
2014
    abstract protected function _fieldData(string $table): array;
2015

2016
    /**
2017
     * Platform-specific index data.
2018
     *
2019
     * @see    getIndexData()
2020
     *
2021
     * @return array<string, stdClass>
2022
     */
2023
    abstract protected function _indexData(string $table): array;
2024

2025
    /**
2026
     * Platform-specific foreign keys data.
2027
     *
2028
     * @see    getForeignKeyData()
2029
     *
2030
     * @return array<string, stdClass>
2031
     */
2032
    abstract protected function _foreignKeyData(string $table): array;
2033

2034
    /**
2035
     * Platform-specific SQL statement to disable foreign key checks.
2036
     *
2037
     * If this feature is not supported, return empty string.
2038
     *
2039
     * @TODO This method should be moved to an interface that represents foreign key support.
2040
     *
2041
     * @return string
2042
     *
2043
     * @see disableForeignKeyChecks()
2044
     */
2045
    protected function _disableForeignKeyChecks()
2046
    {
UNCOV
2047
        return '';
×
2048
    }
2049

2050
    /**
2051
     * Platform-specific SQL statement to enable foreign key checks.
2052
     *
2053
     * If this feature is not supported, return empty string.
2054
     *
2055
     * @TODO This method should be moved to an interface that represents foreign key support.
2056
     *
2057
     * @return string
2058
     *
2059
     * @see enableForeignKeyChecks()
2060
     */
2061
    protected function _enableForeignKeyChecks()
2062
    {
UNCOV
2063
        return '';
×
2064
    }
2065

2066
    /**
2067
     * Accessor for properties if they exist.
2068
     *
2069
     * @return mixed
2070
     */
2071
    public function __get(string $key)
2072
    {
2073
        if (property_exists($this, $key)) {
1,104 ✔
2074
            return $this->{$key};
1,103 ✔
2075
        }
2076

2077
        return null;
1 ✔
2078
    }
2079

2080
    /**
2081
     * Checker for properties existence.
2082
     */
2083
    public function __isset(string $key): bool
2084
    {
2085
        return property_exists($this, $key);
3 ✔
2086
    }
2087
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc