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

codeigniter4 / CodeIgniter4 / 23333481000

20 Mar 2026 07:37AM UTC coverage: 85.764% (+0.04%) from 85.725%
23333481000

Pull #10037

github

web-flow
Merge 256ab0643 into 601cfa9f3
Pull Request #10037: fix: SQLite3 config type handling for `.env` overrides

59 of 61 new or added lines in 1 file covered. (96.72%)

3 existing lines in 1 file now uncovered.

22375 of 26089 relevant lines covered (85.76%)

207.12 hits per line

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

88.19
/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 array      $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      $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|bool $encrypt
41
 * @property-read array      $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 array      $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
 * @see \CodeIgniter\Database\BaseConnectionTest
63
 */
64
abstract class BaseConnection implements ConnectionInterface
65
{
66
    /**
67
     * Cached builtin type names per class/property.
68
     *
69
     * @var array<class-string, array<string, list<string>>>
70
     */
71
    private static array $propertyBuiltinTypesCache = [];
72

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

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

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

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

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

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

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

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

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

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

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

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

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

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

177
    /**
178
     * Encryption flag/data
179
     *
180
     * @var array|bool
181
     */
182
    protected $encrypt = false;
183

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

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

202
    /**
203
     * Settings for a failover connection.
204
     *
205
     * @var array
206
     */
207
    protected $failover = [];
208

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

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

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

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

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

247
    /**
248
     * Identifier escape character
249
     *
250
     * @var array|string
251
     */
252
    public $escapeChar = '"';
253

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

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

268
    /**
269
     * RegExp used to escape identifiers
270
     *
271
     * @var array
272
     */
273
    protected $pregEscapeChar = [];
274

275
    /**
276
     * Holds previously looked up data
277
     * for performance reasons.
278
     *
279
     * @var array
280
     */
281
    public $dataCache = [];
282

283
    /**
284
     * Microtime when connection was made
285
     *
286
     * @var float
287
     */
288
    protected $connectTime = 0.0;
289

290
    /**
291
     * How long it took to establish connection.
292
     *
293
     * @var float
294
     */
295
    protected $connectDuration = 0.0;
296

297
    /**
298
     * If true, no queries will actually be
299
     * run against the database.
300
     *
301
     * @var bool
302
     */
303
    protected $pretend = false;
304

305
    /**
306
     * Transaction enabled flag
307
     *
308
     * @var bool
309
     */
310
    public $transEnabled = true;
311

312
    /**
313
     * Strict transaction mode flag
314
     *
315
     * @var bool
316
     */
317
    public $transStrict = true;
318

319
    /**
320
     * Transaction depth level
321
     *
322
     * @var int
323
     */
324
    protected $transDepth = 0;
325

326
    /**
327
     * Transaction status flag
328
     *
329
     * Used with transactions to determine if a rollback should occur.
330
     *
331
     * @var bool
332
     */
333
    protected $transStatus = true;
334

335
    /**
336
     * Transaction failure flag
337
     *
338
     * Used with transactions to determine if a transaction has failed.
339
     *
340
     * @var bool
341
     */
342
    protected $transFailure = false;
343

344
    /**
345
     * Whether to throw exceptions during transaction
346
     */
347
    protected bool $transException = false;
348

349
    /**
350
     * Array of table aliases.
351
     *
352
     * @var list<string>
353
     */
354
    protected $aliasedTables = [];
355

356
    /**
357
     * Query Class
358
     *
359
     * @var string
360
     */
361
    protected $queryClass = Query::class;
362

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

376
    /**
377
     * Saves our connection settings.
378
     */
379
    public function __construct(array $params)
380
    {
381
        if (isset($params['dateFormat'])) {
435✔
382
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
109✔
383
            unset($params['dateFormat']);
109✔
384
        }
385

386
        $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($params));
435✔
387

388
        foreach ($params as $key => $value) {
435✔
389
            if (property_exists($this, $key)) {
141✔
390
                $this->{$key} = $this->castScalarValueForTypedProperty(
141✔
391
                    $value,
141✔
392
                    $typedPropertyTypes[$key] ?? [],
141✔
393
                );
141✔
394
            }
395
        }
396

397
        $queryClass = str_replace('Connection', 'Query', static::class);
434✔
398

399
        if (class_exists($queryClass)) {
434✔
400
            $this->queryClass = $queryClass;
357✔
401
        }
402

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

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

424
        if ($types === [] || in_array('string', $types, true) || in_array('mixed', $types, true)) {
141✔
425
            return $value;
141✔
426
        }
427

428
        $trimmedValue = trim($value);
5✔
429

430
        if (in_array('null', $types, true) && strtolower($trimmedValue) === 'null') {
5✔
431
            return null;
1✔
432
        }
433

434
        if (in_array('int', $types, true) && preg_match('/^[+-]?\d+$/', $trimmedValue) === 1) {
5✔
435
            return (int) $trimmedValue;
2✔
436
        }
437

438
        if (in_array('float', $types, true) && is_numeric($trimmedValue)) {
4✔
NEW
439
            return (float) $trimmedValue;
×
440
        }
441

442
        if (in_array('bool', $types, true) || in_array('false', $types, true) || in_array('true', $types, true)) {
4✔
443
            $boolValue = filter_var($trimmedValue, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
3✔
444

445
            if ($boolValue !== null) {
3✔
446
                if (in_array('bool', $types, true)) {
3✔
447
                    return $boolValue;
2✔
448
                }
449

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

454
                if ($boolValue === true && in_array('true', $types, true)) {
1✔
455
                    return true;
1✔
456
                }
457
            }
458
        }
459

460
        return $value;
1✔
461
    }
462

463
    /**
464
     * @param list<string> $properties
465
     *
466
     * @return array<string, list<string>>
467
     */
468
    private function getBuiltinPropertyTypesMap(array $properties): array
469
    {
470
        $className = static::class;
435✔
471
        $requested = array_fill_keys($properties, true);
435✔
472

473
        if (! isset(self::$propertyBuiltinTypesCache[$className])) {
435✔
474
            self::$propertyBuiltinTypesCache[$className] = [];
25✔
475
        }
476

477
        // Fill only the properties requested by this call that are not cached yet.
478
        $missing = array_diff_key($requested, self::$propertyBuiltinTypesCache[$className]);
435✔
479

480
        if ($missing !== []) {
435✔
481
            $reflection = new ReflectionClass($className);
26✔
482

483
            foreach ($reflection->getProperties() as $property) {
26✔
484
                $propertyName = $property->getName();
26✔
485

486
                if (! isset($missing[$propertyName])) {
26✔
487
                    continue;
26✔
488
                }
489

490
                $type = $property->getType();
26✔
491

492
                if (! $type instanceof ReflectionType) {
26✔
493
                    self::$propertyBuiltinTypesCache[$className][$propertyName] = [];
25✔
494

495
                    continue;
25✔
496
                }
497

498
                $namedTypes   = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
16✔
499
                $builtinTypes = [];
16✔
500

501
                foreach ($namedTypes as $namedType) {
16✔
502
                    if (! $namedType instanceof ReflectionNamedType || ! $namedType->isBuiltin()) {
16✔
NEW
503
                        continue;
×
504
                    }
505

506
                    $builtinTypes[] = $namedType->getName();
16✔
507
                }
508

509
                if ($type->allowsNull() && ! in_array('null', $builtinTypes, true)) {
16✔
510
                    $builtinTypes[] = 'null';
13✔
511
                }
512

513
                self::$propertyBuiltinTypesCache[$className][$propertyName] = $builtinTypes;
16✔
514
            }
515

516
            // Untyped or unresolved properties are cached as empty to avoid re-reflecting them.
517
            foreach (array_keys($missing) as $propertyName) {
26✔
518
                self::$propertyBuiltinTypesCache[$className][$propertyName] ??= [];
26✔
519
            }
520
        }
521

522
        $typedProperties = [];
435✔
523

524
        foreach ($properties as $property) {
435✔
525
            $typedProperties[$property] = self::$propertyBuiltinTypesCache[$className][$property] ?? [];
141✔
526
        }
527

528
        return $typedProperties;
435✔
529
    }
530

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

550
        $this->connectTime = microtime(true);
49✔
551
        $connectionErrors  = [];
49✔
552

553
        try {
554
            // Connect to the database and set the connection ID
555
            $this->connID = $this->connect($this->pConnect);
49✔
556
        } catch (Throwable $e) {
2✔
557
            $this->connID       = false;
2✔
558
            $connectionErrors[] = sprintf(
2✔
559
                'Main connection [%s]: %s',
2✔
560
                $this->DBDriver,
2✔
561
                $e->getMessage(),
2✔
562
            );
2✔
563
            log_message('error', 'Error connecting to the database: ' . $e);
2✔
564
        }
565

566
        // No connection resource? Check if there is a failover else throw an error
567
        if (! $this->connID) {
49✔
568
            // Check if there is a failover set
569
            if (! empty($this->failover) && is_array($this->failover)) {
4✔
570
                // Go over all the failovers
571
                foreach ($this->failover as $index => $failover) {
2✔
572
                    $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($failover));
2✔
573

574
                    // Replace the current settings with those of the failover
575
                    foreach ($failover as $key => $val) {
2✔
576
                        if (property_exists($this, $key)) {
2✔
577
                            $this->{$key} = $this->castScalarValueForTypedProperty(
2✔
578
                                $val,
2✔
579
                                $typedPropertyTypes[$key] ?? [],
2✔
580
                            );
2✔
581
                        }
582
                    }
583

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

597
                    // If a connection is made break the foreach loop
598
                    if ($this->connID) {
2✔
599
                        break;
2✔
600
                    }
601
                }
602
            }
603

604
            // We still don't have a connection?
605
            if (! $this->connID) {
4✔
606
                throw new DatabaseException(sprintf(
2✔
607
                    'Unable to connect to the database.%s%s',
2✔
608
                    PHP_EOL,
2✔
609
                    implode(PHP_EOL, $connectionErrors),
2✔
610
                ));
2✔
611
            }
612
        }
613

614
        $this->connectDuration = microtime(true) - $this->connectTime;
47✔
615
    }
616

617
    /**
618
     * Close the database connection.
619
     *
620
     * @return void
621
     */
622
    public function close()
623
    {
624
        if ($this->connID) {
3✔
625
            $this->_close();
3✔
626
            $this->connID = false;
3✔
627
        }
628
    }
629

630
    /**
631
     * Keep or establish the connection if no queries have been sent for
632
     * a length of time exceeding the server's idle timeout.
633
     *
634
     * @return void
635
     */
636
    public function reconnect()
637
    {
638
        if ($this->ping() === false) {
2✔
639
            $this->close();
1✔
640
            $this->initialize();
1✔
641
        }
642
    }
643

644
    /**
645
     * Platform dependent way method for closing the connection.
646
     *
647
     * @return void
648
     */
649
    abstract protected function _close();
650

651
    /**
652
     * Check if the connection is still alive.
653
     */
654
    public function ping(): bool
655
    {
656
        if ($this->connID === false) {
5✔
657
            return false;
2✔
658
        }
659

660
        return $this->_ping();
4✔
661
    }
662

663
    /**
664
     * Driver-specific ping implementation.
665
     */
666
    protected function _ping(): bool
667
    {
668
        try {
669
            $result = $this->simpleQuery('SELECT 1');
4✔
670

671
            return $result !== false;
4✔
672
        } catch (DatabaseException) {
×
673
            return false;
×
674
        }
675
    }
676

677
    /**
678
     * Create a persistent database connection.
679
     *
680
     * @return false|TConnection
681
     */
682
    public function persistentConnect()
683
    {
684
        return $this->connect(true);
×
685
    }
686

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

701
    /**
702
     * Returns the name of the current database being used.
703
     */
704
    public function getDatabase(): string
705
    {
706
        return empty($this->database) ? '' : $this->database;
773✔
707
    }
708

709
    /**
710
     * Set DB Prefix
711
     *
712
     * Set's the DB Prefix to something new without needing to reconnect
713
     *
714
     * @param string $prefix The prefix
715
     */
716
    public function setPrefix(string $prefix = ''): string
717
    {
718
        return $this->DBPrefix = $prefix;
13✔
719
    }
720

721
    /**
722
     * Returns the database prefix.
723
     */
724
    public function getPrefix(): string
725
    {
726
        return $this->DBPrefix;
12✔
727
    }
728

729
    /**
730
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
731
     */
732
    public function getPlatform(): string
733
    {
734
        return $this->DBDriver;
23✔
735
    }
736

737
    /**
738
     * Sets the Table Aliases to use. These are typically
739
     * collected during use of the Builder, and set here
740
     * so queries are built correctly.
741
     *
742
     * @return $this
743
     */
744
    public function setAliasedTables(array $aliases)
745
    {
746
        $this->aliasedTables = $aliases;
995✔
747

748
        return $this;
995✔
749
    }
750

751
    /**
752
     * Add a table alias to our list.
753
     *
754
     * @return $this
755
     */
756
    public function addTableAlias(string $alias)
757
    {
758
        if ($alias === '') {
30✔
759
            return $this;
6✔
760
        }
761

762
        if (! in_array($alias, $this->aliasedTables, true)) {
24✔
763
            $this->aliasedTables[] = $alias;
24✔
764
        }
765

766
        return $this;
24✔
767
    }
768

769
    /**
770
     * Executes the query against the database.
771
     *
772
     * @return false|TResult
773
     */
774
    abstract protected function execute(string $sql);
775

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

794
        if (empty($this->connID)) {
827✔
795
            $this->initialize();
24✔
796
        }
797

798
        /**
799
         * @var Query $query
800
         */
801
        $query = new $queryClass($this);
827✔
802

803
        $query->setQuery($sql, $binds, $setEscapeFlags);
827✔
804

805
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
827✔
806
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
807
        }
808

809
        $startTime = microtime(true);
827✔
810

811
        // Always save the last query so we can use
812
        // the getLastQuery() method.
813
        $this->lastQuery = $query;
827✔
814

815
        // If $pretend is true, then we just want to return
816
        // the actual query object here. There won't be
817
        // any results to return.
818
        if ($this->pretend) {
827✔
819
            $query->setDuration($startTime);
10✔
820

821
            return $query;
10✔
822
        }
823

824
        // Run the query for real
825
        try {
826
            $exception      = null;
827✔
827
            $this->resultID = $this->simpleQuery($query->getQuery());
827✔
828
        } catch (DatabaseException $exception) {
16✔
829
            $this->resultID = false;
16✔
830
        }
831

832
        if ($this->resultID === false) {
827✔
833
            $query->setDuration($startTime, $startTime);
35✔
834

835
            // This will trigger a rollback if transactions are being used
836
            $this->handleTransStatus();
35✔
837

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

855
                    if ($transDepth === $this->transDepth) {
2✔
856
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
857
                        break;
×
858
                    }
859
                }
860

861
                // Let others do something with this query.
862
                Events::trigger('DBQuery', $query);
12✔
863

864
                if ($exception instanceof DatabaseException) {
12✔
865
                    throw new DatabaseException(
10✔
866
                        $exception->getMessage(),
10✔
867
                        $exception->getCode(),
10✔
868
                        $exception,
10✔
869
                    );
10✔
870
                }
871

872
                return false;
2✔
873
            }
874

875
            // Let others do something with this query.
876
            Events::trigger('DBQuery', $query);
23✔
877

878
            return false;
23✔
879
        }
880

881
        $query->setDuration($startTime);
827✔
882

883
        // Let others do something with this query
884
        Events::trigger('DBQuery', $query);
827✔
885

886
        // resultID is not false, so it must be successful
887
        if ($this->isWriteType($sql)) {
827✔
888
            return true;
788✔
889
        }
890

891
        // query is not write-type, so it must be read-type query; return QueryResult
892
        $resultClass = str_replace('Connection', 'Result', static::class);
826✔
893

894
        return new $resultClass($this->connID, $this->resultID);
826✔
895
    }
896

897
    /**
898
     * Performs a basic query against the database. No binding or caching
899
     * is performed, nor are transactions handled. Simply takes a raw
900
     * query string and returns the database-specific result id.
901
     *
902
     * @return false|TResult
903
     */
904
    public function simpleQuery(string $sql)
905
    {
906
        if (empty($this->connID)) {
834✔
907
            $this->initialize();
6✔
908
        }
909

910
        return $this->execute($sql);
834✔
911
    }
912

913
    /**
914
     * Disable Transactions
915
     *
916
     * This permits transactions to be disabled at run-time.
917
     *
918
     * @return void
919
     */
920
    public function transOff()
921
    {
922
        $this->transEnabled = false;
×
923
    }
924

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

943
        return $this;
4✔
944
    }
945

946
    /**
947
     * Start Transaction
948
     */
949
    public function transStart(bool $testMode = false): bool
950
    {
951
        if (! $this->transEnabled) {
49✔
952
            return false;
×
953
        }
954

955
        return $this->transBegin($testMode);
49✔
956
    }
957

958
    /**
959
     * If set to true, exceptions are thrown during transactions.
960
     *
961
     * @return $this
962
     */
963
    public function transException(bool $transException)
964
    {
965
        $this->transException = $transException;
3✔
966

967
        return $this;
3✔
968
    }
969

970
    /**
971
     * Complete Transaction
972
     */
973
    public function transComplete(): bool
974
    {
975
        if (! $this->transEnabled) {
49✔
976
            return false;
×
977
        }
978

979
        // The query() function will set this flag to FALSE in the event that a query failed
980
        if ($this->transStatus === false || $this->transFailure === true) {
49✔
981
            $this->transRollback();
16✔
982

983
            // If we are NOT running in strict mode, we will reset
984
            // the _trans_status flag so that subsequent groups of
985
            // transactions will be permitted.
986
            if ($this->transStrict === false) {
16✔
987
                $this->transStatus = true;
4✔
988
            }
989

990
            return false;
16✔
991
        }
992

993
        return $this->transCommit();
38✔
994
    }
995

996
    /**
997
     * Lets you retrieve the transaction flag to determine if it has failed
998
     */
999
    public function transStatus(): bool
1000
    {
1001
        return $this->transStatus;
15✔
1002
    }
1003

1004
    /**
1005
     * Begin Transaction
1006
     */
1007
    public function transBegin(bool $testMode = false): bool
1008
    {
1009
        if (! $this->transEnabled) {
52✔
1010
            return false;
×
1011
        }
1012

1013
        // When transactions are nested we only begin/commit/rollback the outermost ones
1014
        if ($this->transDepth > 0) {
52✔
1015
            $this->transDepth++;
×
1016

1017
            return true;
×
1018
        }
1019

1020
        if (empty($this->connID)) {
52✔
1021
            $this->initialize();
×
1022
        }
1023

1024
        // Reset the transaction failure flag.
1025
        // If the $testMode flag is set to TRUE transactions will be rolled back
1026
        // even if the queries produce a successful result.
1027
        $this->transFailure = $testMode;
52✔
1028

1029
        if ($this->_transBegin()) {
52✔
1030
            $this->transDepth++;
52✔
1031

1032
            return true;
52✔
1033
        }
1034

1035
        return false;
×
1036
    }
1037

1038
    /**
1039
     * Commit Transaction
1040
     */
1041
    public function transCommit(): bool
1042
    {
1043
        if (! $this->transEnabled || $this->transDepth === 0) {
38✔
1044
            return false;
×
1045
        }
1046

1047
        // When transactions are nested we only begin/commit/rollback the outermost ones
1048
        if ($this->transDepth > 1 || $this->_transCommit()) {
38✔
1049
            $this->transDepth--;
38✔
1050

1051
            return true;
38✔
1052
        }
1053

1054
        return false;
×
1055
    }
1056

1057
    /**
1058
     * Rollback Transaction
1059
     */
1060
    public function transRollback(): bool
1061
    {
1062
        if (! $this->transEnabled || $this->transDepth === 0) {
19✔
1063
            return false;
×
1064
        }
1065

1066
        // When transactions are nested we only begin/commit/rollback the outermost ones
1067
        if ($this->transDepth > 1 || $this->_transRollback()) {
19✔
1068
            $this->transDepth--;
19✔
1069

1070
            return true;
19✔
1071
        }
1072

1073
        return false;
×
1074
    }
1075

1076
    /**
1077
     * Reset transaction status - to restart transactions after strict mode failure
1078
     */
1079
    public function resetTransStatus(): static
1080
    {
1081
        $this->transStatus = true;
4✔
1082

1083
        return $this;
4✔
1084
    }
1085

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

1098
    /**
1099
     * Begin Transaction
1100
     */
1101
    abstract protected function _transBegin(): bool;
1102

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

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

1113
    /**
1114
     * Returns a non-shared new instance of the query builder for this connection.
1115
     *
1116
     * @param array|string|TableName $tableName
1117
     *
1118
     * @return BaseBuilder
1119
     *
1120
     * @throws DatabaseException
1121
     */
1122
    public function table($tableName)
1123
    {
1124
        if (empty($tableName)) {
937✔
1125
            throw new DatabaseException('You must set the database table to be used with your query.');
×
1126
        }
1127

1128
        $className = str_replace('Connection', 'Builder', static::class);
937✔
1129

1130
        return new $className($tableName, $this);
937✔
1131
    }
1132

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

1143
        return $builder;
14✔
1144
    }
1145

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

1170
        $this->pretend();
16✔
1171

1172
        $sql = $func($this);
16✔
1173

1174
        $this->pretend(false);
16✔
1175

1176
        if ($sql instanceof QueryInterface) {
16✔
1177
            $sql = $sql->getOriginalQuery();
16✔
1178
        }
1179

1180
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
16✔
1181
        /** @var BasePreparedQuery $class */
1182
        $class = new $class($this);
16✔
1183

1184
        return $class->prepare($sql, $options);
16✔
1185
    }
1186

1187
    /**
1188
     * Returns the last query's statement object.
1189
     *
1190
     * @return Query
1191
     */
1192
    public function getLastQuery()
1193
    {
1194
        return $this->lastQuery;
11✔
1195
    }
1196

1197
    /**
1198
     * Returns a string representation of the last query's statement object.
1199
     */
1200
    public function showLastQuery(): string
1201
    {
1202
        return (string) $this->lastQuery;
×
1203
    }
1204

1205
    /**
1206
     * Returns the time we started to connect to this database in
1207
     * seconds with microseconds.
1208
     *
1209
     * Used by the Debug Toolbar's timeline.
1210
     */
1211
    public function getConnectStart(): ?float
1212
    {
1213
        return $this->connectTime;
1✔
1214
    }
1215

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

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

1260
        if (is_array($item)) {
1,100✔
1261
            $escapedArray = [];
1✔
1262

1263
            foreach ($item as $k => $v) {
1✔
1264
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1265
            }
1266

1267
            return $escapedArray;
1✔
1268
        }
1269

1270
        if ($item instanceof TableName) {
1,100✔
1271
            /** @psalm-suppress NoValue I don't know why ERROR. */
1272
            return $this->escapeTableName($item);
2✔
1273
        }
1274

1275
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1276
        $item = (string) $item;
1,100✔
1277

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

1290
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1291
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
1,089✔
1292
            /** @psalm-suppress NoValue I don't know why ERROR. */
1293
            return $item;
100✔
1294
        }
1295

1296
        // Convert tabs or multiple spaces into single spaces
1297
        /** @psalm-suppress NoValue I don't know why ERROR. */
1298
        $item = preg_replace('/\s+/', ' ', trim($item));
1,088✔
1299

1300
        // If the item has an alias declaration we remove it and set it aside.
1301
        // Note: strripos() is used in order to support spaces in table names
1302
        if ($offset = strripos($item, ' AS ')) {
1,088✔
1303
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1304
            $item  = substr($item, 0, $offset);
11✔
1305
        } elseif ($offset = strrpos($item, ' ')) {
1,083✔
1306
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
12✔
1307
            $item  = substr($item, 0, $offset);
12✔
1308
        } else {
1309
            $alias = '';
1,077✔
1310
        }
1311

1312
        // Break the string apart if it contains periods, then insert the table prefix
1313
        // in the correct location, assuming the period doesn't indicate that we're dealing
1314
        // with an alias. While we're at it, we will escape the components
1315
        if (str_contains($item, '.')) {
1,088✔
1316
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
135✔
1317
        }
1318

1319
        // In some cases, especially 'from', we end up running through
1320
        // protect_identifiers twice. This algorithm won't work when
1321
        // it contains the escapeChar so strip it out.
1322
        $item = trim($item, $this->escapeChar);
1,080✔
1323

1324
        // Is there a table prefix? If not, no need to insert it
1325
        if ($this->DBPrefix !== '') {
1,080✔
1326
            // Verify table prefix and replace if necessary
1327
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
828✔
1328
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1329
            }
1330
            // Do we prefix an item with no segments?
1331
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
828✔
1332
                $item = $this->DBPrefix . $item;
821✔
1333
            }
1334
        }
1335

1336
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
1,080✔
1337
            $item = $this->escapeIdentifiers($item);
1,078✔
1338
        }
1339

1340
        return $item . $alias;
1,080✔
1341
    }
1342

1343
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1344
    {
1345
        $parts = explode('.', $item);
135✔
1346

1347
        // Does the first segment of the exploded item match
1348
        // one of the aliases previously identified? If so,
1349
        // we have nothing more to do other than escape the item
1350
        //
1351
        // NOTE: The ! empty() condition prevents this method
1352
        // from breaking when QB isn't enabled.
1353
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
135✔
1354
            if ($protectIdentifiers) {
10✔
1355
                foreach ($parts as $key => $val) {
10✔
1356
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
10✔
1357
                        $parts[$key] = $this->escapeIdentifiers($val);
10✔
1358
                    }
1359
                }
1360

1361
                $item = implode('.', $parts);
10✔
1362
            }
1363

1364
            return $item . $alias;
10✔
1365
        }
1366

1367
        // Is there a table prefix defined in the config file? If not, no need to do anything
1368
        if ($this->DBPrefix !== '') {
129✔
1369
            // We now add the table prefix based on some logic.
1370
            // Do we have 4 segments (hostname.database.table.column)?
1371
            // If so, we add the table prefix to the column name in the 3rd segment.
1372
            if (isset($parts[3])) {
121✔
1373
                $i = 2;
×
1374
            }
1375
            // Do we have 3 segments (database.table.column)?
1376
            // If so, we add the table prefix to the column name in 2nd position
1377
            elseif (isset($parts[2])) {
121✔
1378
                $i = 1;
×
1379
            }
1380
            // Do we have 2 segments (table.column)?
1381
            // If so, we add the table prefix to the column name in 1st segment
1382
            else {
1383
                $i = 0;
121✔
1384
            }
1385

1386
            // This flag is set when the supplied $item does not contain a field name.
1387
            // This can happen when this function is being called from a JOIN.
1388
            if ($fieldExists === false) {
121✔
1389
                $i++;
×
1390
            }
1391

1392
            // Verify table prefix and replace if necessary
1393
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
121✔
1394
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1395
            }
1396
            // We only add the table prefix if it does not already exist
1397
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
121✔
1398
                $parts[$i] = $this->DBPrefix . $parts[$i];
121✔
1399
            }
1400

1401
            // Put the parts back together
1402
            $item = implode('.', $parts);
121✔
1403
        }
1404

1405
        if ($protectIdentifiers) {
129✔
1406
            $item = $this->escapeIdentifiers($item);
129✔
1407
        }
1408

1409
        return $item . $alias;
129✔
1410
    }
1411

1412
    /**
1413
     * Escape the SQL Identifier
1414
     *
1415
     * This function escapes single identifier.
1416
     *
1417
     * @param non-empty-string|TableName $item
1418
     */
1419
    public function escapeIdentifier($item): string
1420
    {
1421
        if ($item === '') {
720✔
1422
            return '';
×
1423
        }
1424

1425
        if ($item instanceof TableName) {
720✔
1426
            return $this->escapeTableName($item);
7✔
1427
        }
1428

1429
        return $this->escapeChar
720✔
1430
            . str_replace(
720✔
1431
                $this->escapeChar,
720✔
1432
                $this->escapeChar . $this->escapeChar,
720✔
1433
                $item,
720✔
1434
            )
720✔
1435
            . $this->escapeChar;
720✔
1436
    }
1437

1438
    /**
1439
     * Returns escaped table name with alias.
1440
     */
1441
    private function escapeTableName(TableName $tableName): string
1442
    {
1443
        $alias = $tableName->getAlias();
7✔
1444

1445
        return $this->escapeIdentifier($tableName->getActualTableName())
7✔
1446
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
7✔
1447
    }
1448

1449
    /**
1450
     * Escape the SQL Identifiers
1451
     *
1452
     * This function escapes column and table names
1453
     *
1454
     * @param array|string $item
1455
     *
1456
     * @return ($item is array ? array : string)
1457
     */
1458
    public function escapeIdentifiers($item)
1459
    {
1460
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
1,106✔
1461
            return $item;
5✔
1462
        }
1463

1464
        if (is_array($item)) {
1,105✔
1465
            foreach ($item as $key => $value) {
733✔
1466
                $item[$key] = $this->escapeIdentifiers($value);
733✔
1467
            }
1468

1469
            return $item;
733✔
1470
        }
1471

1472
        // Avoid breaking functions and literal values inside queries
1473
        if (ctype_digit($item)
1,105✔
1474
            || $item[0] === "'"
1,104✔
1475
            || ($this->escapeChar !== '"' && $item[0] === '"')
1,104✔
1476
            || str_contains($item, '(')) {
1,105✔
1477
            return $item;
47✔
1478
        }
1479

1480
        if ($this->pregEscapeChar === []) {
1,104✔
1481
            if (is_array($this->escapeChar)) {
301✔
1482
                $this->pregEscapeChar = [
×
1483
                    preg_quote($this->escapeChar[0], '/'),
×
1484
                    preg_quote($this->escapeChar[1], '/'),
×
1485
                    $this->escapeChar[0],
×
1486
                    $this->escapeChar[1],
×
1487
                ];
×
1488
            } else {
1489
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
301✔
1490
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
301✔
1491
            }
1492
        }
1493

1494
        foreach ($this->reservedIdentifiers as $id) {
1,104✔
1495
            /** @psalm-suppress NoValue I don't know why ERROR. */
1496
            if (str_contains($item, '.' . $id)) {
1,104✔
1497
                return preg_replace(
3✔
1498
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1499
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1500
                    $item,
3✔
1501
                );
3✔
1502
            }
1503
        }
1504

1505
        /** @psalm-suppress NoValue I don't know why ERROR. */
1506
        return preg_replace(
1,102✔
1507
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
1,102✔
1508
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
1,102✔
1509
            $item,
1,102✔
1510
        );
1,102✔
1511
    }
1512

1513
    /**
1514
     * Prepends a database prefix if one exists in configuration
1515
     *
1516
     * @throws DatabaseException
1517
     */
1518
    public function prefixTable(string $table = ''): string
1519
    {
1520
        if ($table === '') {
3✔
1521
            throw new DatabaseException('A table name is required for that operation.');
×
1522
        }
1523

1524
        return $this->DBPrefix . $table;
3✔
1525
    }
1526

1527
    /**
1528
     * Returns the total number of rows affected by this query.
1529
     */
1530
    abstract public function affectedRows(): int;
1531

1532
    /**
1533
     * "Smart" Escape String
1534
     *
1535
     * Escapes data based on type.
1536
     * Sets boolean and null types
1537
     *
1538
     * @param array|bool|float|int|object|string|null $str
1539
     *
1540
     * @return ($str is array ? array : float|int|string)
1541
     */
1542
    public function escape($str)
1543
    {
1544
        if (is_array($str)) {
922✔
1545
            return array_map($this->escape(...), $str);
749✔
1546
        }
1547

1548
        if ($str instanceof Stringable) {
922✔
1549
            if ($str instanceof RawSql) {
15✔
1550
                return $str->__toString();
14✔
1551
            }
1552

1553
            $str = (string) $str;
1✔
1554
        }
1555

1556
        if (is_string($str)) {
919✔
1557
            return "'" . $this->escapeString($str) . "'";
868✔
1558
        }
1559

1560
        if (is_bool($str)) {
840✔
1561
            return ($str === false) ? 0 : 1;
8✔
1562
        }
1563

1564
        return $str ?? 'NULL';
838✔
1565
    }
1566

1567
    /**
1568
     * Escape String
1569
     *
1570
     * @param list<string|Stringable>|string|Stringable $str  Input string
1571
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1572
     *
1573
     * @return list<string>|string
1574
     */
1575
    public function escapeString($str, bool $like = false)
1576
    {
1577
        if (is_array($str)) {
868✔
1578
            foreach ($str as $key => $val) {
×
1579
                $str[$key] = $this->escapeString($val, $like);
×
1580
            }
1581

1582
            return $str;
×
1583
        }
1584

1585
        if ($str instanceof Stringable) {
868✔
1586
            if ($str instanceof RawSql) {
2✔
1587
                return $str->__toString();
×
1588
            }
1589

1590
            $str = (string) $str;
2✔
1591
        }
1592

1593
        $str = $this->_escapeString($str);
868✔
1594

1595
        // escape LIKE condition wildcards
1596
        if ($like) {
868✔
1597
            return str_replace(
2✔
1598
                [
2✔
1599
                    $this->likeEscapeChar,
2✔
1600
                    '%',
2✔
1601
                    '_',
2✔
1602
                ],
2✔
1603
                [
2✔
1604
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1605
                    $this->likeEscapeChar . '%',
2✔
1606
                    $this->likeEscapeChar . '_',
2✔
1607
                ],
2✔
1608
                $str,
2✔
1609
            );
2✔
1610
        }
1611

1612
        return $str;
868✔
1613
    }
1614

1615
    /**
1616
     * Escape LIKE String
1617
     *
1618
     * Calls the individual driver for platform
1619
     * specific escaping for LIKE conditions
1620
     *
1621
     * @param list<string|Stringable>|string|Stringable $str
1622
     *
1623
     * @return list<string>|string
1624
     */
1625
    public function escapeLikeString($str)
1626
    {
1627
        return $this->escapeString($str, true);
2✔
1628
    }
1629

1630
    /**
1631
     * Platform independent string escape.
1632
     *
1633
     * Will likely be overridden in child classes.
1634
     */
1635
    protected function _escapeString(string $str): string
1636
    {
1637
        return str_replace("'", "''", remove_invisible_characters($str, false));
822✔
1638
    }
1639

1640
    /**
1641
     * This function enables you to call PHP database functions that are not natively included
1642
     * in CodeIgniter, in a platform independent manner.
1643
     *
1644
     * @param array ...$params
1645
     *
1646
     * @throws DatabaseException
1647
     */
1648
    public function callFunction(string $functionName, ...$params): bool
1649
    {
1650
        $driver = $this->getDriverFunctionPrefix();
2✔
1651

1652
        if (! str_starts_with($functionName, $driver)) {
2✔
1653
            $functionName = $driver . $functionName;
1✔
1654
        }
1655

1656
        if (! function_exists($functionName)) {
2✔
1657
            if ($this->DBDebug) {
×
1658
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1659
            }
1660

1661
            return false;
×
1662
        }
1663

1664
        return $functionName(...$params);
2✔
1665
    }
1666

1667
    /**
1668
     * Get the prefix of the function to access the DB.
1669
     */
1670
    protected function getDriverFunctionPrefix(): string
1671
    {
1672
        return strtolower($this->DBDriver) . '_';
×
1673
    }
1674

1675
    // --------------------------------------------------------------------
1676
    // META Methods
1677
    // --------------------------------------------------------------------
1678

1679
    /**
1680
     * Returns an array of table names
1681
     *
1682
     * @return false|list<string>
1683
     *
1684
     * @throws DatabaseException
1685
     */
1686
    public function listTables(bool $constrainByPrefix = false)
1687
    {
1688
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
781✔
1689
            return $constrainByPrefix
775✔
1690
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1691
                : $this->dataCache['table_names'];
775✔
1692
        }
1693

1694
        $sql = $this->_listTables($constrainByPrefix);
57✔
1695

1696
        if ($sql === false) {
57✔
1697
            if ($this->DBDebug) {
×
1698
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1699
            }
1700

1701
            return false;
×
1702
        }
1703

1704
        $this->dataCache['table_names'] = [];
57✔
1705

1706
        $query = $this->query($sql);
57✔
1707

1708
        foreach ($query->getResultArray() as $row) {
57✔
1709
            /** @var string $table */
1710
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
54✔
1711

1712
            $this->dataCache['table_names'][] = $table;
54✔
1713
        }
1714

1715
        return $this->dataCache['table_names'];
57✔
1716
    }
1717

1718
    /**
1719
     * Determine if a particular table exists
1720
     *
1721
     * @param bool $cached Whether to use data cache
1722
     */
1723
    public function tableExists(string $tableName, bool $cached = true): bool
1724
    {
1725
        if ($cached) {
775✔
1726
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
774✔
1727
        }
1728

1729
        if (false === ($sql = $this->_listTables(false, $tableName))) {
728✔
1730
            if ($this->DBDebug) {
×
1731
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1732
            }
1733

1734
            return false;
×
1735
        }
1736

1737
        $tableExists = $this->query($sql)->getResultArray() !== [];
728✔
1738

1739
        // if cache has been built already
1740
        if (! empty($this->dataCache['table_names'])) {
728✔
1741
            $key = array_search(
724✔
1742
                strtolower($tableName),
724✔
1743
                array_map(strtolower(...), $this->dataCache['table_names']),
724✔
1744
                true,
724✔
1745
            );
724✔
1746

1747
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1748
            // OR if table does exist but is not found in cache
1749
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
724✔
1750
                $this->resetDataCache();
1✔
1751
            }
1752
        }
1753

1754
        return $tableExists;
728✔
1755
    }
1756

1757
    /**
1758
     * Fetch Field Names
1759
     *
1760
     * @param string|TableName $tableName
1761
     *
1762
     * @return false|list<string>
1763
     *
1764
     * @throws DatabaseException
1765
     */
1766
    public function getFieldNames($tableName)
1767
    {
1768
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
12✔
1769

1770
        // Is there a cached result?
1771
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1772
            return $this->dataCache['field_names'][$table];
7✔
1773
        }
1774

1775
        if (empty($this->connID)) {
8✔
1776
            $this->initialize();
×
1777
        }
1778

1779
        if (false === ($sql = $this->_listColumns($tableName))) {
8✔
1780
            if ($this->DBDebug) {
×
1781
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1782
            }
1783

1784
            return false;
×
1785
        }
1786

1787
        $query = $this->query($sql);
8✔
1788

1789
        $this->dataCache['field_names'][$table] = [];
8✔
1790

1791
        foreach ($query->getResultArray() as $row) {
8✔
1792
            // Do we know from where to get the column's name?
1793
            if (! isset($key)) {
8✔
1794
                if (isset($row['column_name'])) {
8✔
1795
                    $key = 'column_name';
8✔
1796
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1797
                    $key = 'COLUMN_NAME';
8✔
1798
                } else {
1799
                    // We have no other choice but to just get the first element's key.
1800
                    $key = key($row);
8✔
1801
                }
1802
            }
1803

1804
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1805
        }
1806

1807
        return $this->dataCache['field_names'][$table];
8✔
1808
    }
1809

1810
    /**
1811
     * Determine if a particular field exists
1812
     */
1813
    public function fieldExists(string $fieldName, string $tableName): bool
1814
    {
1815
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1816
    }
1817

1818
    /**
1819
     * Returns an object with field data
1820
     *
1821
     * @return list<stdClass>
1822
     */
1823
    public function getFieldData(string $table)
1824
    {
1825
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
145✔
1826
    }
1827

1828
    /**
1829
     * Returns an object with key data
1830
     *
1831
     * @return array<string, stdClass>
1832
     */
1833
    public function getIndexData(string $table)
1834
    {
1835
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
160✔
1836
    }
1837

1838
    /**
1839
     * Returns an object with foreign key data
1840
     *
1841
     * @return array<string, stdClass>
1842
     */
1843
    public function getForeignKeyData(string $table)
1844
    {
1845
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
37✔
1846
    }
1847

1848
    /**
1849
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1850
     *
1851
     * @return array<string, stdClass>
1852
     *
1853
     * array[
1854
     *    {constraint_name} =>
1855
     *        stdClass[
1856
     *            'constraint_name'     => string,
1857
     *            'table_name'          => string,
1858
     *            'column_name'         => string[],
1859
     *            'foreign_table_name'  => string,
1860
     *            'foreign_column_name' => string[],
1861
     *            'on_delete'           => string,
1862
     *            'on_update'           => string,
1863
     *            'match'               => string
1864
     *        ]
1865
     * ]
1866
     */
1867
    protected function foreignKeyDataToObjects(array $data)
1868
    {
1869
        $retVal = [];
37✔
1870

1871
        foreach ($data as $row) {
37✔
1872
            $name = $row['constraint_name'];
12✔
1873

1874
            // for sqlite generate name
1875
            if ($name === null) {
12✔
1876
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
1877
            }
1878

1879
            $obj                      = new stdClass();
12✔
1880
            $obj->constraint_name     = $name;
12✔
1881
            $obj->table_name          = $row['table_name'];
12✔
1882
            $obj->column_name         = $row['column_name'];
12✔
1883
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
1884
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
1885
            $obj->on_delete           = $row['on_delete'];
12✔
1886
            $obj->on_update           = $row['on_update'];
12✔
1887
            $obj->match               = $row['match'];
12✔
1888

1889
            $retVal[$name] = $obj;
12✔
1890
        }
1891

1892
        return $retVal;
37✔
1893
    }
1894

1895
    /**
1896
     * Disables foreign key checks temporarily.
1897
     *
1898
     * @return bool
1899
     */
1900
    public function disableForeignKeyChecks()
1901
    {
1902
        $sql = $this->_disableForeignKeyChecks();
742✔
1903

1904
        if ($sql === '') {
742✔
1905
            // The feature is not supported.
1906
            return false;
×
1907
        }
1908

1909
        return $this->query($sql);
742✔
1910
    }
1911

1912
    /**
1913
     * Enables foreign key checks temporarily.
1914
     *
1915
     * @return bool
1916
     */
1917
    public function enableForeignKeyChecks()
1918
    {
1919
        $sql = $this->_enableForeignKeyChecks();
825✔
1920

1921
        if ($sql === '') {
825✔
1922
            // The feature is not supported.
1923
            return false;
×
1924
        }
1925

1926
        return $this->query($sql);
825✔
1927
    }
1928

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

1941
        return $this;
17✔
1942
    }
1943

1944
    /**
1945
     * Empties our data cache. Especially helpful during testing.
1946
     *
1947
     * @return $this
1948
     */
1949
    public function resetDataCache()
1950
    {
1951
        $this->dataCache = [];
36✔
1952

1953
        return $this;
36✔
1954
    }
1955

1956
    /**
1957
     * Determines if the statement is a write-type query or not.
1958
     *
1959
     * @param string $sql
1960
     */
1961
    public function isWriteType($sql): bool
1962
    {
1963
        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);
851✔
1964
    }
1965

1966
    /**
1967
     * Returns the last error code and message.
1968
     *
1969
     * Must return an array with keys 'code' and 'message':
1970
     *
1971
     * @return array{code: int|string|null, message: string|null}
1972
     */
1973
    abstract public function error(): array;
1974

1975
    /**
1976
     * Insert ID
1977
     *
1978
     * @return int|string
1979
     */
1980
    abstract public function insertID();
1981

1982
    /**
1983
     * Generates the SQL for listing tables in a platform-dependent manner.
1984
     *
1985
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1986
     *
1987
     * @return false|string
1988
     */
1989
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1990

1991
    /**
1992
     * Generates a platform-specific query string so that the column names can be fetched.
1993
     *
1994
     * @param string|TableName $table
1995
     *
1996
     * @return false|string
1997
     */
1998
    abstract protected function _listColumns($table = '');
1999

2000
    /**
2001
     * Platform-specific field data information.
2002
     *
2003
     * @see getFieldData()
2004
     *
2005
     * @return list<stdClass>
2006
     */
2007
    abstract protected function _fieldData(string $table): array;
2008

2009
    /**
2010
     * Platform-specific index data.
2011
     *
2012
     * @see    getIndexData()
2013
     *
2014
     * @return array<string, stdClass>
2015
     */
2016
    abstract protected function _indexData(string $table): array;
2017

2018
    /**
2019
     * Platform-specific foreign keys data.
2020
     *
2021
     * @see    getForeignKeyData()
2022
     *
2023
     * @return array<string, stdClass>
2024
     */
2025
    abstract protected function _foreignKeyData(string $table): array;
2026

2027
    /**
2028
     * Platform-specific SQL statement to disable foreign key checks.
2029
     *
2030
     * If this feature is not supported, return empty string.
2031
     *
2032
     * @TODO This method should be moved to an interface that represents foreign key support.
2033
     *
2034
     * @return string
2035
     *
2036
     * @see disableForeignKeyChecks()
2037
     */
2038
    protected function _disableForeignKeyChecks()
2039
    {
2040
        return '';
×
2041
    }
2042

2043
    /**
2044
     * Platform-specific SQL statement to enable foreign key checks.
2045
     *
2046
     * If this feature is not supported, return empty string.
2047
     *
2048
     * @TODO This method should be moved to an interface that represents foreign key support.
2049
     *
2050
     * @return string
2051
     *
2052
     * @see enableForeignKeyChecks()
2053
     */
2054
    protected function _enableForeignKeyChecks()
2055
    {
2056
        return '';
×
2057
    }
2058

2059
    /**
2060
     * Accessor for properties if they exist.
2061
     *
2062
     * @return array|bool|float|int|object|resource|string|null
2063
     */
2064
    public function __get(string $key)
2065
    {
2066
        if (property_exists($this, $key)) {
1,076✔
2067
            return $this->{$key};
1,075✔
2068
        }
2069

2070
        return null;
1✔
2071
    }
2072

2073
    /**
2074
     * Checker for properties existence.
2075
     */
2076
    public function __isset(string $key): bool
2077
    {
2078
        return property_exists($this, $key);
250✔
2079
    }
2080
}
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